Odoo Assistant

io.github.singleflo由 GitHub 帳號 singleflo 發布v0.3.15更新於 Oct 29, 2026

Odoo virtual employee via MCP: query, create, modify records, workflows, PDFs, and activities.

已驗證Streamable HTTP可網頁執行效率與工作流溝通與協作商業與電商

概覽

AI 產生的概覽

把助理連接到 Odoo ERP 實例,讓它能查詢、讀取、建立與修改業務記錄、執行工作流程並與同事溝通。

可以做什麼
  • 跨 Odoo 模型搜尋、讀取、計數與分組記錄,超長文字欄位可分視窗讀取
  • 檢視實例概況:版本、公司、模組,以及模型的欄位與建立必填項
  • 建立與更新記錄,執行確認、過帳等工作流程步驟,並在允許時取消記錄
  • 安排活動,並在記錄的訊息區張貼通知指定使用者的訊息
  • 下載記錄的所有文件並產生其 PDF
  • 列出可聯絡對象、讀取 Discuss 對話,並在預覽獲准後傳送私訊或頻道訊息
適用情境
適合讓助理直接在公司 Odoo 資料中工作:回答關於訂單、發票、潛在客戶或庫存的問題,草擬並建立記錄,或推動單據走完工作流程步驟。連接正式環境時建議只開啟唯讀模式。
執行需求
可透過 uvx 或 pip 在本機執行(Python),需設定 ODOO_BASE_URL 與 ODOO_API_KEY;也可使用託管的遠端端點,以 Odoo 網址和 API 金鑰登入。Odoo Online 必須設定 ODOO_DB,ODOO_USER 為選用。由於僅支援 API 金鑰驗證,需 Odoo 14 以上版本。
安裝前請注意
預設情況下助理可以建立、寫入、確認訂單、過帳發票並傳訊息給使用者;若要唯讀請將 ODOO_MCP_ALLOW 設為 none。只有設定 ODOO_MCP_ALLOW_UNLINK 才能刪除,且設定 ODOO_MCP_DENY 會整份取代預設拒絕清單。API 金鑰承襲其 Odoo 使用者的權限,應在 Odoo 端限制。託管服務會加密儲存你的 Odoo 網址與 API 金鑰。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Odoo Assistant,將其新增到工作區。
  2. 連接 所需的 憑證。它們會被私密儲存,絕不會在公開頁面上展示。
  3. 為需要使用其工具的對話啟用該服務。

Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。

交給代理程式安裝

把這段提示詞貼上到 Claude Code、Codex、Cursor 等代理程式,或 SourceWeft 對話中。代理程式會讀取該伺服器的安裝指南,先向你展示發布者、端點和登入方式,經你同意後再把它新增到你的客戶端。金鑰不會寫進任何命令。

閱讀 https://sourceweft.com/mcp/io.github.singleflo%2Fodoo-assistant/install.md,並按其中說明把這個 MCP 伺服器接入我的客戶端。修改任何設定之前,先告訴我它的發布者、端點和登入方式,等我確認。不要把 API 金鑰寫進命令或我會提交的檔案;問我金鑰應從哪裡取得。

檢視代理程式所依據的安裝指南

其他 MCP 客戶端

把它新增到你客戶端的 mcpServers 設定中。 客戶端會在首次連接時完成 OAuth 登入。

{
  "mcpServers": {
    "odoo-assistant": {
      "type": "http",
      "url": "https://odoomcp.singleflo.com/mcp"
    }
  }
}

README

Singleflo for Odoo

An Odoo virtual employee via the Model Context Protocol (MCP), published as the odoo-assistant package. This server exposes Odoo's business logic, records, and workflows to LLMs, allowing them to query, create, update, and act on Odoo data safely.

Singleflo for Odoo is an independent project, not affiliated with, endorsed by or sponsored by Odoo S.A. Odoo is a trademark of Odoo S.A., named here only to say which software this server works with.

Quickstart

Install with your AI agent. If you already have an AI assistant on your computer — Hermes, Claude Code, Claude Desktop, Codex, Cursor, opencode, any of the hosts below — paste this prompt into it as one message:

text
Please install "Singleflo for Odoo" (the MCP server `odoo-assistant`) so that you can work with my Odoo.Open and follow, step by step, the instructions written for you at:https://raw.githubusercontent.com/singleflo/odoo-assistant-mcp/main/docs/INSTALL-WITH-YOUR-AGENT.mdI am not technical. Talk to me in English, give me one simple step at a time and wait for my answer, and do all the technical work yourself.When you need the Odoo API key, guide me through creating it screen by screen. Never ask me for my Odoo password.At the end, check that you can read my Odoo and tell me what you set up.

The same prompt in Italian, Spanish and Czech is in docs/INSTALL-PROMPT.md. The page it links is written for the agent rather than for you: it asks you for the Odoo address, walks you through creating the API key, finds the database name on Odoo Online, installs uv, writes the configuration file its own host reads, and verifies the connection. In Claude.ai or ChatGPT, which cannot install anything, it guides you to the hosted connection instead. The manual route is below.

1. Install

Run the server directly:

bash
uvx odoo-assistant

Or install it into your environment:

bash
uv pip install odoo-assistant

Installing from source for development remains possible:

bash
uv pip install git+https://github.com/singleflo/odoo-assistant-mcp

2. Configure Environment Variables

Don't have an API key yet?

How to create an Odoo API key → — seven steps, with a screenshot of each.

In short: avatar → Preferences → Account Security → New API Key. It is never your account password, it belongs to one Odoo user and carries exactly that user's permissions, and Odoo shows its value exactly once.

  • ODOO_BASE_URL: Mandatory always. The base URL of your Odoo instance, with no trailing slash (e.g., https://mycompany.odoo.com).
  • ODOO_API_KEY: Mandatory always. The Odoo API key (Odoo 14+ — see the box above, or docs/api-key.md). An account password is not accepted. A key is per-user, scoped, and revocable on its own. Odoo 19 additionally requires a description and an expiry, max 3 months.
  • ODOO_DB: Mandatory on Odoo Online (SaaS, *.odoo.com), optional elsewhere. On Odoo Online, the database-list endpoint is disabled. Discovery cannot find the name, and every tool call fails with an opaque "Error executing tool" without hinting that the database is the problem. With ODOO_DB set, the same config connects immediately. The SaaS database name is not the pretty subdomain — it carries a suffix, in the shape mycompany16-prod-12345678 — and you find it at /web/database/selector or in the Odoo.com account page. Elsewhere, it is discovered automatically when the instance serves exactly one database, and is required when it serves several.
  • ODOO_USER: Never mandatory. Omitted, the client probes res.users for uid 1 to 59 and keeps the one the key answers for. This adds up to 59 extra round trips on the first call, and it fails outright if the key owner's uid is 60 or higher. Setting it removes that cost. It must be the login (e.g. [email protected]), and a wrong value makes Odoo's authenticate() return False rather than raise — which reads like a permission error.
  • ODOO_MCP_ALLOW: Optional, default * — every method the deny list does not refuse. The single value none makes the server read-only, which is what you want when pointing an agent at live company data for reading. Anything else is a comma-separated list of method names. See "What the agent may do" below.
  • ODOO_MCP_DENY: Optional. Unset, it is the default deny list — unlink, archive, action_cancel, button_cancel, action_reverse, action_draft, mailing.mailing:action_send. A value you set replaces that list entirely. See "What the agent may do" below.
  • ODOO_MCP_ALLOW_UNLINK: Optional, off by default. yes, true or 1 (any case) grants unlink; no entry on either list can. See "What the agent may do" below.
  • ODOO_MCP_DATA_DIR: Optional. Where this server keeps everything it writes, the instance profiles included. It defaults to the platform's own data directory — %LOCALAPPDATA%\odoo-assistant on Windows, ~/Library/Application Support/odoo-assistant on macOS, and $XDG_DATA_HOME/odoo-assistant (else ~/.local/share/odoo-assistant) elsewhere.

What the agent may do

Every write and every action passes a gate before it reaches Odoo. The gate judges a call by its METHOD NAME against two lists you own, in the host configuration file, and reads both from the process environment at call time:

  • ODOO_MCP_ALLOW — what may run. Unset or empty means *: every method the deny list does not refuse. The single value none makes the server read-only.
  • ODOO_MCP_DENY — what may not. Unset or empty means the default list: unlink, archive, action_cancel, button_cancel, action_reverse, action_draft, mailing.mailing:action_send. A value you set replaces that list entirely rather than extending it, so the refusals in force are exactly the names in your own file — ODOO_MCP_DENY=unlink is how you re-admit action_cancel.

An entry is either method, which matches that method on every model, or model:method, which matches it on one. Matching is exact string equality — no prefix, no substring, so action_send never matches action_send_and_print and action_cancel never matches button_cancel. A human read and approved those exact names, and a looser match would let lookalikes through that nobody saw. Deny is checked before allow, so a name on both lists refuses.

Model qualification is what the measured case needs. On one live instance 31 models answer to action_send, and only the Evolution wizards should — the one on mailing.mailing can email an entire customer base in a single call. That is why the default deny list spells that entry mailing.mailing:action_send and leaves action_send working everywhere else.

Four rules sit outside the lists:

  • unlink is decided before both of them. No value of ODOO_MCP_ALLOW or ODOO_MCP_DENY can ever grant deletion; only ODOO_MCP_ALLOW_UNLINK does — yes, true or 1, any case. Deletion is the one action that cannot be undone, and a name in a comma-separated list must never be enough to grant it.
  • archive is a virtual name. Both action_archive and a write carrying active: False carry it into the lists, so denying archive refuses hiding records however they are spelled.
  • Private methods are always refused, whatever the lists say. Odoo itself rejects every method starting with _, so no list could deliver one.
  • Reads never pass through either list, because a read has no effect for a list to govern. The account.move structural guard is untouched by any of this and still applies to them: a query mixing invoices, bills and journal entries is refused whatever the lists hold, because a meaningless read is its own hazard.
ConfigurationWhat it permits
ODOO_MCP_ALLOW=noneReads only. Nothing this server does can change a record — the setting for an agent pointed at live production data.
Nothing setDefault. Every method except the seven on the default deny list: creating, writing, confirming orders, posting invoices, scheduling activities, messaging users.
ODOO_MCP_ALLOW_UNLINK=yesThe default, plus unlink. The deny list is unchanged, so archive and action_cancel stay refused until you set ODOO_MCP_DENY yourself.

The lists are set out of band, by a human, and the model running against this server cannot change them. When the gate refuses, the reason names the call, the entry that decided it and the variable that would change the answer, so the agent can explain what the operation would have changed and leave the decision to you.

Note that this is the authority of this server, not of the account. An agent with shell access can always bypass an MCP server by invoking Odoo directly. A limit that must hold regardless of the client belongs in the Odoo access rights of the user the API key belongs to, where the Odoo server enforces it.

Database and login: when you must set them

Only ODOO_BASE_URL and ODOO_API_KEY are required everywhere. ODOO_DB and ODOO_USER are discovered, and whether that discovery can succeed depends on how your instance is hosted.

The database is looked for in two steps, in this order: list() on /xmlrpc/db, then a /web/session/get_session_info POST, which needs no credentials and still reports the database name when list_db = False hides the first one. The login is never asked for — an API key belongs to exactly one user, and execute_kw accepts it only with that user's uid, so the client finds the owner by probing res.users for uid 1 through 59.

HostingODOO_DBODOO_USERWhy
Odoo Online (*.odoo.com, SaaS)RequiredOptionalMeasured: the database-list endpoint is disabled there, and without the name every tool call fails with an opaque "Error executing tool" that gives no hint the database is the problem. The SaaS name is not the subdomain — it carries a suffix, in the shape mycompany16-prod-12345678, and you find it at /web/database/selector.
Odoo.shOptionalOptionalA branch serves one database, and the session-info fallback reports its name. Untested against a live branch: set it if the first call fails.
On-premise, one databaseOptionalOptionalDiscovery returns the single name, from either step.
On-premise, several databasesOptional, but convenientOptionalNot required: the discovery error names the databases it found and tells you to pick one, so the failure is self-explanatory. Setting it skips that round trip and removes the ambiguity.

ODOO_USER is never mandatory by itself, on any hosting. Setting it saves up to 59 discovery round trips on the first call of a session, and it becomes required when the API key owner's uid is 60 or higher, because discovery only probes uid 1 to 59. It must be the login (e.g. [email protected]), and a wrong value is quiet in a way that misleads: Odoo's authenticate() returns False for an unknown login rather than raising, so a typo reads like a permission error and not like a typo.

By Odoo version

Odoo 14 through 18 behave identically here. XML-RPC carries the database name in every execute_kw call, so the client must know it before it can authenticate at all — which is exactly why discovery exists.

Odoo 19 adds the JSON-2 API, which selects the database with an X-Odoo-Database HTTP header. The header table on Odoo's own page lists it as optional, and the page's Database section is specific about when it stops being: it is "required when a single Odoo server hosts multiple databases and the dbfilter wasn't configured to use the Host header", or, as the same page puts it elsewhere, the database "must only be provided (via the X-Odoo-Database HTTP header) on systems where there are multiple databases available for a same domain". Where the hostname already picks the database — Odoo Online, Odoo.sh, any dbfilter keyed on Host — it can be left out.

This server's client tries JSON-2 first: one POST /json/2/res.users/search_count carrying Authorization: Bearer <key>, and a 200 makes JSON-2 the transport for the session; anything else falls back to XML-RPC. It sends Authorization and Content-Type and nothing else, so it does not set X-Odoo-Database — over JSON-2 the host has to resolve the database itself, and ODOO_DB reaches only the XML-RPC path.

Odoo Version Support

Odoo 14.0 is the absolute minimum supported version because this server authenticates using API keys only, which do not exist in Odoo 13 or earlier.

Odoo VersionAPI KeysXML-RPCOfficially Maintained (Aug 2026)Support Level / Notes
≤ 13.0NoYesNoUnsupported. API keys do not exist, so this server cannot authenticate.
14.0YesYesNoProtocol-compatible. Untested against a live instance.
15.0YesYesNoProtocol-compatible. Untested against a live instance.
16.0YesYesNoVerified against a live Enterprise instance: connection, authentication, reads, instance_overview and the Discuss tools. Two generational differences are handled for you — see the note below. Write scenarios were not exercised.
17.0YesYesYes (until Sep 2026)Protocol-compatible. Untested against a live instance.
18.0YesYesYes (until Sep 2027)Primary target. Verified and fully supported against a live instance.
19.0YesYesYes (until Sep 2028)Protocol-compatible. Untested against a live instance. API keys require description and expiry (max 3 months). The JSON-2 API selects the database with an X-Odoo-Database header — see "Database and login: when you must set them" above.

Two things changed between Odoo 16 and 17, and neither needs configuration:

  • Discuss was renamed. mail.channel / mail.channel.member became discuss.channel / discuss.channel.member in 17. The server asks the instance which pair it has and uses that, so the four Discuss tools work on both generations.
  • Subscriptions moved onto sale.order, which before 17 had no subscription_state field at all. On 16 that section is simply absent from instance_overview — an absence, not a failure.

API Key Generation Path

To generate an API key, log in to your Odoo instance and navigate to: Preferences / My Profile → Account Security → New API Key

The illustrated walkthrough is docs/api-key.md, which also covers the duration field, the Odoo 19 expiry rule and how to revoke a key.

Transport & Deprecation Note

The client automatically detects if the native JSON-2 API is available at /json/2/<model>/<method> (which uses Authorization: bearer <API_KEY>) and falls back to XML-RPC if it is not. Please note that XML-RPC and JSON-RPC are deprecated in Odoo 19 and scheduled for removal in Odoo 22.

Sources

Tools and Resources

The server exposes 22 tools and 2 resource types:

Tools

  1. search_read: Search and read records in one call (Odoo search_read).
  2. read_record: Read one record by id, always with named fields.
  3. read_long_field: Read one long text field in windows, so a value larger than the 5000-character result cap stays readable to the end.
  4. count_records: Count the records matching a domain (Odoo search_count).
  5. group_records: Group records and count (or aggregate) per bucket in one call (Odoo read_group) — totals per state, stage or month.
  6. instance_overview: Summarise the connected instance: version, companies, volumes per area, in-house modules, anomalies.
  7. required_fields: List what Odoo demands before a create on a model, the default it would apply, and how existing records actually use it.
  8. describe_model: List a model's fields as the live instance defines them: names, types, relations, selection values, required marked.
  9. create_record: Create a record, reusing an existing match when unique_on is given.
  10. write_record: Write field values to one record and report what actually changed.
  11. run_action: Run a workflow method and report the state it left behind.
  12. cancel_record: Cancel a record through action_cancel, following the wizard it returns.
  13. notify_user: Notify users on a record's chatter. Internal by default. Previews first and posts only with the user's approval (see below).
  14. create_activity: Schedule an activity: the only notification that carries a deadline.
  15. download_docs: Save every document of a record to disk, chatter files included.
  16. generate_pdf: Render the PDF of a record and return where it was saved.
  17. list_message_targets: List who can be messaged and where, including internal users with presence (online/away/offline) and the caller's open conversations. Ask this before sending.
  18. read_conversation: Read a Discuss conversation, newest first.
  19. send_direct_message: Send a 1-to-1 Discuss message that appears in the user's chat systray in real time. This sends no email and reaches them whatever their notification setting says. Previews first and sends only with the user's approval.
  20. send_channel_message: Post to an existing Discuss channel, refusing a room that holds a non-employee. Previews first and posts only with the user's approval.
  21. explore_module: Discover a module's structure by interrogating the live instance.
  22. list_known_modules: List the modules this server has learned: name, generation date, records.

Tools 13-14 (notify_user, create_activity) notify ABOUT a record and land in the Inbox bell; tools 17-20 are Discuss conversations that land in the chat systray. "Message user X" is the second kind, which uses send_direct_message, not notify_user.

The three tools that speak for the user — notify_user, send_direct_message, send_channel_message — take two calls. The first, without a confirmation_token, sends nothing: it validates the request and answers with a preview (recipients, route, who would get an email, the full message) and a token. The agent shows the preview and asks; only after the user approves does it call again with the same arguments and the token. The token approves exactly the previewed arguments, once, for ten minutes, so a message changed after approval is refused rather than sent.

Resources

  • odoo://skill: Access the Odoo assistant skill instructions.
  • odoo://ref/*: Access generated reference documentation for explored modules.

Host Configuration Examples

Every example below carries only what matters: the two required variables, and the database, which discovery cannot reach on Odoo Online. The login is discovered, and the gate keeps its defaults unless you add ODOO_MCP_ALLOW or ODOO_MCP_DENY — see "What the agent may do". Note the quotes: environment values are strings.

ODOO_DB appears in every snippet because it is the variable most people are missing when nothing works. Set it only when required — see "Database and login: when you must set them" above. JSON allows no comments, so that note lives here rather than inside the blocks; the TOML and YAML snippets carry it inline.

Each snippet below was checked against that host's own documentation, cited on the Source: line under it. Where a host has a one-line add command, it is given as well, because it writes the same entry without a hand-edited file.

Hosted server: Claude.ai, ChatGPT and Codex

Everything in the sections below runs the server on your machine. There is a second way into the same 22 tools: they are also served over the internet at https://odoomcp.singleflo.com/mcp, which Claude.ai, ChatGPT and Codex can reach directly. Nothing is installed and no environment variables are set on your side — you sign in once with your Odoo address and API key, on the server's consent page, and the chat you already use reaches your Odoo. The local configuration of every other section keeps working exactly as written; the two ways differ only in where the server runs.

The ChatGPT Plugin Directory listing connects to https://odoomcp.singleflo.com/openai/mcp instead: the same server, sign-in and policy, with every tool except run_action and explore_module, which OpenAI does not admit: the first runs any workflow method, the second picks between two operations with one argument. Claude.ai, Codex and ChatGPT developer mode keep using /mcp with all 22.

The consent page asks for your Odoo URL, your API key, and one choice: read lets the agent look at your data, standard lets it also create records, run workflows and schedule activities. Deletion is never available through the hosted server — unlink is not reachable from it under either choice. The choice can be changed later by signing in again and picking the other one.

Two optional fields sit alongside them, for the same reasons ODOO_DB and ODOO_USER exist locally: the database, required on Odoo Online, and the Odoo login of the user the key belongs to. Leave the login empty and the server works the owner out by probing uid 1 to 59; fill it in when that probe cannot reach — a user created well after the instance was set up sits past uid 59, and there the sign-in fails until the login is given. The page says so when it happens.

What the hosted server stores: your Odoo URL and API key, encrypted at rest; your sign-in identity, kept only as a hash; and the files a tool produces, which live there only as links that expire after fifteen minutes. The details are at https://odoomcp.singleflo.com/privacy, with https://odoomcp.singleflo.com/terms and https://odoomcp.singleflo.com/support on the same domain.

Per-host instructions: Claude.ai and ChatGPT are hosted-only connections and have their own sections below. Claude Code and Codex keep their local configuration above and gained a hosted one-liner each.

If you would rather run the hosted part yourself, the developer guide at docs/REMOTE.md covers the whole path — local run, tunnel, deployment.

Claude Desktop

Claude Desktop ships for macOS and Windows only, and keeps its servers in claude_desktop_config.json. Reach it from Settings → Developer → Edit Config, or edit it where it lives:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

Quit Claude Desktop completely and reopen it: it reads the file at startup and does not reload it. Server logs land in ~/Library/Logs/Claude/ on macOS and %APPDATA%\Claude\logs on Windows, one file per server, and stdio servers write everything they say to stderr there.

Source: https://modelcontextprotocol.io/docs/develop/connect-local-servers

Claude Code

One line adds the server. The -- separates Claude Code's own options from the command that starts the server, and everything after it is passed through untouched:

bash
claude mcp add --env ODOO_BASE_URL=https://mycompany.odoo.com \  --env ODOO_API_KEY=your-api-key-here \  --env ODOO_DB=mycompany16-prod-12345678 \  --transport stdio odoo-assistant -- uvx odoo-assistant

Note the order. --env takes KEY=value pairs and keeps reading them, so the server name must not follow it directly — put at least one other option, here --transport stdio, in between, or the CLI reads odoo-assistant as another pair and rejects it.

--scope decides where the entry lands: local (the default: this project, you only), project (.mcp.json at the repo root, committed and shared), or user (every project). To write it by hand, the same entry goes under mcpServers in .mcp.json or in ~/.claude.json:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      },      "timeout": 120000    }  }}

The per-server timeout is a wall-clock limit per tool call, in milliseconds, and overrides the MCP_TOOL_TIMEOUT environment variable for this server alone; MCP_TIMEOUT, also milliseconds, bounds server startup instead. Neither matters here except on the first instance_overview call of a session, which pays for authentication plus dozens of XML-RPC round trips. Reconnect the server from the /mcp panel after editing, or restart Claude Code.

The one-liner above runs the server on your machine. Claude Code can also use the hosted server — no install, no environment variables:

bash
claude mcp add --transport http odoo-assistant https://odoomcp.singleflo.com/mcp

The first tool call starts the sign-in and lands on the consent page, where read or standard is chosen. On the hosted route the gate is decided there, not by local ODOO_MCP_* variables, and deletion is not offered at all.

Source: https://code.claude.com/docs/en/mcp

OpenAI Codex CLI

Codex keeps MCP servers in TOML, in ~/.codex/config.toml, or in a project's .codex/config.toml once you have trusted that project. The table is spelled with an underscore — mcp_servers, not mcp.servers. The ChatGPT desktop app, the Codex CLI and the IDE extension all read this one file, so configuring it once covers the three.

bash
codex mcp add odoo-assistant \  --env ODOO_BASE_URL=https://mycompany.odoo.com \  --env ODOO_API_KEY=your-api-key-here \  --env ODOO_DB=mycompany16-prod-12345678 \  -- uvx odoo-assistant

The same entry written out:

toml
[mcp_servers.odoo-assistant]command = "uvx"args = ["odoo-assistant"]startup_timeout_sec = 30tool_timeout_sec = 300
[mcp_servers.odoo-assistant.env]ODOO_BASE_URL = "https://mycompany.odoo.com"ODOO_API_KEY = "your-api-key-here"# only when required, see "Database and login" aboveODOO_DB = "mycompany16-prod-12345678"

Both timeouts are in seconds: startup_timeout_sec defaults to 10 and tool_timeout_sec to 60. Only the first instance_overview call comes near either, which is why both are raised above. After editing, press Restart on the server in the desktop app or the IDE extension; in the CLI, start a new session and check it with /mcp.

The hosted server is added by URL instead, and the sign-in is its own command:

bash
codex mcp add odoo-assistant --url https://odoomcp.singleflo.com/mcpcodex mcp login odoo-assistant

codex mcp login walks the same OAuth flow and lands on the consent page, where read or standard is chosen; codex mcp logout odoo-assistant ends the connection. As on every hosted route: reads always work, writes follow the choice made at sign-in, and deletion is not available at all.

Source: https://developers.openai.com/codex/mcp

Source: https://developers.openai.com/codex/config-file/config-reference

ChatGPT

ChatGPT reaches the hosted server through developer mode, available to Pro, Plus, Business, Enterprise and Education accounts, on the web:

  1. In ChatGPT, open Settings → Security and login and turn on Developer mode.
  2. Open chatgpt.com/plugins, press the plus button and create a developer-mode app for the MCP URL https://odoomcp.singleflo.com/mcp.
  3. ChatGPT starts the sign-in, which lands on the server's consent page: enter your Odoo URL and API key and choose read or standard.
  4. In a conversation, choose Developer mode from the plus menu and select the Singleflo for Odoo app.

There is deliberately no local snippet here: developer mode connects to remote MCP servers over HTTPS only, so the stdio configuration of the other sections does not apply. A public listing in ChatGPT's Plugin Directory, which removes the developer-mode step, is planned but has not arrived yet.

What the agent may do is decided once, at sign-in: read, or standard — never deletion, whatever the conversation asks for.

Source: https://developers.openai.com/plugins/deploy/connect-chatgpt

Claude.ai (web, Desktop, mobile)

Claude connects to the hosted server as a custom connector. Open Customize → Connectors → Add custom connector, paste https://odoomcp.singleflo.com/mcp as the server URL and confirm. On Team and Enterprise plans an owner adds it once under Organization settings → Connectors; members then connect from Customize → Connectors.

The first use starts the sign-in, which lands on the consent page: your Odoo URL, your API key, and the read-or-standard choice.

This link opens the same dialog with the name and URL already filled in — review them and confirm; nothing is added until you do:

text
https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Singleflo%20for%20Odoo&connectorUrl=https%3A%2F%2Fodoomcp.singleflo.com%2Fmcp

Source: https://claude.com/docs/connectors/custom/remote-mcp

Source: https://claude.com/docs/connectors/building/directory-vs-custom

opencode

Add this to opencode.json or .opencode/opencode.json in your project, or to ~/.config/opencode/opencode.json to make the server available everywhere:

json
{  "$schema": "https://opencode.ai/config.json",  "mcp": {    "odoo-assistant": {      "type": "local",      "enabled": true,      "command": [        "uvx",        "odoo-assistant"      ],      "timeout": 120000,      "environment": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

opencode's shape differs from the hosts above in ways it rejects outright. The key is mcp (not mcpServers), type is required, command is a single array holding the program and its arguments (there is no separate args), and the environment block is environment (not env).

Set timeout deliberately. It defaults to 5000 ms. The first call of a session pays for authentication plus, for instance_overview, dozens of XML-RPC round trips, which easily exceeds five seconds against a real instance. Set it to 120000.

opencode reads its config once at startup and does not hot-reload it. Quit and restart after editing. Anything you change here, the allow and deny lists included, takes effect only on the next launch.

Source: https://opencode.ai/docs/mcp-servers

Hermes

Hermes keeps its servers in YAML, under mcp_servers: in ~/.hermes/config.yaml:

yaml
mcp_servers:  odoo-assistant:    command: /Users/you/.local/bin/uvx    args:      - odoo-assistant    env:      ODOO_BASE_URL: https://mycompany.odoo.com      ODOO_API_KEY: your-api-key-here      # only when required, see "Database and login" above      ODOO_DB: mycompany16-prod-12345678    timeout: 120    connect_timeout: 60    enabled: true

Three details this shape does not forgive. command is a string and takes only the program, with the arguments in a separate args list — the opposite of opencode's single array. The environment block is env. And the command needs an absolute path: Hermes runs as a desktop application, which does not inherit the PATH of your shell, so a bare uvx is not found.

Both timeouts here are in seconds, not milliseconds: timeout is the tool-call limit and defaults to 300, connect_timeout bounds the initial connection and defaults to 60. The 120 above is comfortably more than the first instance_overview call needs. Reload the servers with /reload-mcp after editing rather than restarting.

hermes mcp add can write this entry for you — its signature is add <name> [--url URL] [--command CMD] [--auth oauth|header] [--args ...] — but pass --args last: it takes the remaining argv, so anything after it is swallowed into args, which is how credentials end up there and the server starts with none.

Source: https://hermes-agent.nousresearch.com/docs/reference/mcp-config-reference

Cursor

Add this to .cursor/mcp.json in your project, to ~/.cursor/mcp.json to make the server available everywhere, or configure it from Customize in the sidebar:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

Cursor interpolates ${env:NAME} inside command, args, env, url and headers, so "ODOO_API_KEY": "${env:ODOO_API_KEY}" keeps the key out of a file you might commit. When a call fails, the reason is in the Output panel under MCP Logs.

Source: https://cursor.com/docs/context/mcp

Windsurf

Windsurf's Cascade agent reads one global file — ~/.codeium/windsurf/mcp_config.json — on every platform. There is no project-scoped equivalent, so this entry applies to every workspace you open:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

Open it from the MCPs icon in the Cascade panel, or from Settings → Cascade → MCP Servers, then refresh the server list. The file interpolates ${env:VAR_NAME} and ${file:/path/to/file} in command, args and env, so the API key can live outside it. Cascade caps the agent at 100 tools in total, and this server contributes 22.

Source: https://docs.windsurf.com/windsurf/cascade/mcp

VS Code and GitHub Copilot

VS Code's root key is servers, not mcpServers — an entry copied from another host's documentation will not be seen. Put it in .vscode/mcp.json in your workspace, to commit it with the project, or run MCP: Open User Configuration from the Command Palette for the copy that follows your user profile into every workspace:

json
{  "servers": {    "odoo-assistant": {      "type": "stdio",      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

The command line writes the same entry:

bash
code --add-mcp "{\"name\":\"odoo-assistant\",\"command\":\"uvx\",\"args\":[\"odoo-assistant\"]}"

The first time VS Code starts a server it asks whether you trust it; decline and the server never runs. Use the code lenses in mcp.json, or MCP: List Servers in the Command Palette, to start, stop and restart it and to read its output. Avoid hardcoding the API key in a committed workspace file — VS Code provides input variables for exactly this.

Source: https://code.visualstudio.com/docs/copilot/customization/mcp-servers

Gemini CLI

Gemini CLI reads mcpServers from settings.json: ~/.gemini/settings.json for every session, or .gemini/settings.json in a project's root for that project only, which takes precedence.

bash
gemini mcp add odoo-assistant uvx odoo-assistant \  --env ODOO_BASE_URL=https://mycompany.odoo.com \  --env ODOO_API_KEY=your-api-key-here \  --env ODOO_DB=mycompany16-prod-12345678 \  --scope user

The same entry written out:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      },      "timeout": 600000    }  }}

timeout is the request timeout in milliseconds and already defaults to 600000, ten minutes, so the first instance_overview call needs nothing from you here; the line is shown only because it is the key to lower if you want a faster failure. Two other habits pay off: Gemini CLI redacts anything matching *KEY*, *TOKEN* or *SECRET* from the inherited environment before spawning a server, so a variable must be named in this env block to arrive at all, and "$MY_VAR" inside it expands from your shell. Restart the CLI after editing, then check the server with /mcp.

Source: https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md

Source: https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/cli-reference.md

Cline

Cline's CLI reads ~/.cline/mcp.json. In the IDE extensions, open the MCP Servers icon in the Cline panel, go to the Configure tab and press Configure MCP Servers, which opens the extension's own settings JSON. Both use the same mcpServers shape:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      },      "disabled": false,      "autoApprove": []    }  }}

Leave autoApprove empty. It is the list of tools that run without asking, and the gate in this server is not a substitute for reading a write call before it happens. cline mcp opens an interactive wizard that adds, edits, enables and removes servers without touching the file. The request timeout is a per-server setting in the MCP settings panel rather than a key in this file — raise it there if the first instance_overview call times out, and restart the server from the same panel.

Source: https://docs.cline.bot/mcp/mcp-overview

Roo Code

Roo Code reads two files: a global mcp_settings.json, opened by the Edit Global MCP button at the bottom of the MCP settings view, and a per-project .roo/mcp.json opened by Edit Project MCP next to it, which Roo creates if it does not exist. A server name present in both takes its project definition.

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      },      "alwaysAllow": [],      "disabled": false,      "timeout": 300    }  }}

timeout here is in seconds, not milliseconds — it accepts 1 to 3600 and defaults to 60. Sixty is enough for every call but the first instance_overview of a session, which is the one to raise it for; the same value is the Network Timeout dropdown in the server's own panel. Leave alwaysAllow empty, for the reason given under Cline. Press the restart button next to the server after editing.

Committing .roo/mcp.json shares the server with your team — so put the API key in a system environment variable and reference it as ${env:ODOO_API_KEY} inside args, rather than writing it into a file that goes into version control.

Source: https://docs.roocode.com/features/mcp/using-mcp-in-roo

Zed

Zed calls them context servers, and the key is context_servers, not mcpServers. Add the entry to your settings file — Command Palette, zed: open settings file — or let Zed write it for you from Settings → AI → MCP Servers → Add Server → Add Local Server:

json
{  "context_servers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

The indicator dot beside the server's name in Settings → AI → MCP Servers says whether it came up: green, with "Server is active" in its tooltip, means Zed reached it. Tool approval is governed by agent.tool_permissions.default, which is "confirm" by default; per-tool rules use the key format mcp:odoo-assistant:<tool_name>, for example mcp:odoo-assistant:search_read.

Source: https://zed.dev/docs/ai/mcp

JetBrains AI Assistant

JetBrains AI Assistant takes the configuration through a dialog rather than a file you locate yourself. Go to Settings | Tools | AI Assistant | Model Context Protocol (MCP), click Add, choose STDIO, and paste this as the JSON configuration:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678"      }    }  }}

The dialog documents command and args, and adds two fields of its own beside the JSON: Working directory, and Server level, which decides whether the server is available globally or only in the current project. Click OK, then Apply — that is what actually starts the server, and the Status column reports whether it connected. If you already have this server in Claude Desktop, Import from Claude carries the whole entry over instead, including its environment block.

Source: https://www.jetbrains.com/help/ai-assistant/mcp.html

Odoo Online Production (Read-Only Example)

If you are connecting to a production instance hosted on Odoo Online (SaaS), you must set ODOO_DB and should set ODOO_MCP_ALLOW to "none" for safety. Here is how it looks in Claude Desktop:

json
{  "mcpServers": {    "odoo-assistant": {      "command": "uvx",      "args": [        "odoo-assistant"      ],      "env": {        "ODOO_BASE_URL": "https://mycompany.odoo.com",        "ODOO_API_KEY": "your-api-key-here",        "ODOO_DB": "mycompany16-prod-12345678",        "ODOO_MCP_ALLOW": "none"      }    }  }}

Setting ODOO_DB is mandatory to bypass the disabled database-list endpoint on Odoo Online, while ODOO_MCP_ALLOW set to "none" ensures the agent cannot modify live production data.

The examples omit the optional variables. Set ODOO_DB when the instance serves several databases, ODOO_USER — the login, e.g. [email protected] — to skip the uid probe, and ODOO_MCP_ALLOW / ODOO_MCP_DENY when the gate's defaults — every method but the seven denied ones — are not what you want.

Changelog

What changed in each release is in CHANGELOG.md, kept there rather than repeated here so the two cannot drift.

License

This project is licensed under the MIT License. See the LICENSE file for details.

來源:README.md,提交 dd0a0fc

工具

22

search_read

Search records

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. One line before you call: rows you only intend to count are a wasted read — `count_records` answers totals, `group_records` answers totals per value, and neither moves records through the context window. And when a SINGLE field is larger than the 5000-character cap, paging with `offset` returns the same cut text forever: `read_long_field` walks that one field in windows instead. 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.

輸入參數limitmodel必填domain必填fieldsoffsetcompany_ids

read_record

Read a record

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. The answer is capped at 5000 characters. When ONE field is bigger than that on its own — a description holding a transcript, a long `body` — asking for fewer fields cannot help: use `read_long_field`, which walks a single field in windows to its end. 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.

輸入參數model必填fieldsrecord_id必填

read_long_field

Read a long text field

Read 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 a 4000-character window of it, naming the total length and the offset of the next window, so the value can be walked to the end one call at a time. An HTML field — an email `body`, a note — comes back as its plain text: tags, styles and scripts are dropped, paragraphs and list items keep their own lines, and offsets count characters of that text. For anything else — several fields, several records — `read_record` and `search_read` stay the tools. Args: model: Odoo model, e.g. "crm.lead". record_id: The record's database id. field: The single text field to walk, e.g. "description". offset: Character to start from. 0 is the beginning.

輸入參數field必填model必填offsetrecord_id必填

count_records

Count records

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. One value per call is all this gives. For a breakdown — how many per state, per stage, per month — use `group_records`, which returns every bucket's count in one call instead of one call per bucket. 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].

輸入參數model必填domaincompany_ids

group_records

Group records

Group 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, year. aggregate: optional specs like ["amount_total_signed:sum"] — also :avg, :min, :max. On `account.move` / `account.move.line`, `amount_total` is refused: it is expressed in each record's own currency, and summing it once inflated a total 11,9×. Aggregate `amount_total_signed`, the company-currency twin, instead. company_ids: Companies to read from, e.g. [1, 2]. On a multi-company instance, omitting this reports one company as the whole business.

輸入參數model必填domain必填group_by必填aggregatecompany_ids

instance_overview

Instance overview

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.

輸入參數refresh

required_fields

Required fields for create

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

輸入參數model必填

describe_model

Describe a model

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

輸入參數model必填

create_record

Create a record

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

輸入參數model必填values必填unique_on

write_record

Update a record

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

輸入參數model必填values必填record_id必填

run_action

Run a workflow action

Run 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 input; it is not a finished result. What may run is decided by the connection's write policy, not by this call: a read-only connection refuses every step; cancelling, archiving, reversing, resetting to draft and mass mailing are refused by default; deletion is refused unless a local operator enabled it, and never on the hosted server. When a step is refused, tell the user the reason the refusal gives and stop; never try another method name to get around it.

輸入參數model必填method必填record_ids必填

cancel_record

Cancel a record

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

輸入參數model必填record_id必填

notify_user

Notify users on a record

Write 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 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 sho

輸入參數forcemodel必填message必填subtypeuser_ids必填record_id必填confirmation_token

create_activity

Schedule an activity

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.

輸入參數daysmodel必填summary必填user_id必填record_id必填activity_type

download_docs

Download a record's documents

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". 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 platform's temporary directory — "/tmp" does not exist on Windows.

輸入參數model必填dest_dirrecord_id必填

generate_pdf

Generate a record's PDF

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` 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. dest_dir: directory to write the PDF into. Defaults to this platform's temporary directory — "/tmp" does not exist on Windows.

輸入參數model必填dest_dirrecord_id必填

list_message_targets

List message targets

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_conversation

Read a conversation

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.

輸入參數limitchannel_id必填

send_direct_message

Send a direct message

Send 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, `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. confirmation_token: empty for the preview; the preview's token to send.

輸入參數message必填user_id必填confirmation_token

send_channel_message

Post to a channel

Post 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 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. confirmation_token: empty for the preview; the preview's token to post.

輸入參數message必填channel_id必填confirmation_token

explore_module

Explore a module

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, 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 script's own grouping for `module_name`.

輸入參數actionmodelsmodule_name必填

list_known_modules

List known modules

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

無需輸入參數。

版本歷史

2
  1. v0.3.15最新Oct 22, 2026
  2. v0.2.0Sep 16, 2026

評分

已滿足適用檢查項的 100%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通必需25 分
  • 已宣告認證方式必需10 分
  • 已列出工具15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分