# Odoo 19 MCP Server (JSON-2 API) [![odoo19-mcp-server MCP server](https://glama.ai/mcp/servers/twtrubiks/odoo19-mcp-server/badges/card.svg)](https://glama.ai/mcp/servers/twtrubiks/odoo19-mcp-server) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.13-blue?logo=python&logoColor=white)](https://www.python.org/) [![GitHub stars](https://img.shields.io/github/stars/twtrubiks/odoo19-mcp-server?style=flat)](https://github.com/twtrubiks/odoo19-mcp-server/stargazers) [![GitHub last commit](https://img.shields.io/github/last-commit/twtrubiks/odoo19-mcp-server)](https://github.com/twtrubiks/odoo19-mcp-server/commits/main) [![Awesome MCP Servers](https://img.shields.io/badge/Awesome-MCP_Servers-fc60a8?logo=awesomelists&logoColor=white)](https://github.com/punkpeye/awesome-mcp-servers) **支援的 MCP Client** [![Claude Code](https://img.shields.io/badge/Claude_Code-supported-D97757?logo=anthropic&logoColor=white)](#claude-code) [![Gemini CLI](https://img.shields.io/badge/Gemini_CLI-supported-4285F4?logo=googlegemini&logoColor=white)](#gemini-cli) [![Antigravity CLI](https://img.shields.io/badge/Antigravity_CLI-supported-1A73E8?logo=google&logoColor=white)](#antigravity-cli) [![OpenClaw](https://img.shields.io/badge/OpenClaw-supported-7C3AED)](#openclaw) [![Codex CLI](https://img.shields.io/badge/Codex_CLI-supported-000000?logo=openai&logoColor=white)](#codex-cli) * [Youtube Tutorial - MCP Server 自己做!Odoo 19 + FastMCP 完整開發教學](https://youtu.be/JhAudIIII3M) Odoo 19 MCP Server,使用 JSON-2 API 連線。 **✨ 支援多 user**:設定 `MCP_MULTIUSER=true` 後,每個 client 以**自己的 Odoo API key** 認證,權限、操作歸屬、審計紀錄都對應真實 user,不再全部掛在同一個服務帳號上,詳見[多 user 模式](#多-user-模式mcp_multiuser)。 本專案基於 [Odoo 19 JSON-2 API 完整使用指南](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/19.0/odoo-json2-client/README.md) 開發。 ![執行畫面](https://cdn.imgpile.com/f/re0866c_xl.png) ## 技術棧 - **Python**: 3.13 - **FastMCP**: >=4.1.0,<5.0.0 - **odoo-client-lib**: 2.0.1 (JSON-2 API) ## 架構 ```mermaid flowchart TB subgraph Client["MCP Client"] CC[Claude Code] GC[Gemini CLI] MI[MCP Inspector] end subgraph Server["MCP Server (FastMCP)"] R[Resources
odoo://models
odoo://user
odoo://company] T[Tools
search_records
create_record
update_record] DI[Dependency Injection
get_caller_client] end subgraph RPC["OdooJsonRpcClient"] OL[odoolib
json2/json2s protocol] end subgraph Odoo["Odoo Server"] EP["/jsonrpc endpoint"] end Client -->|MCP Protocol
stdio/http/sse| Server R --> DI T --> DI DI --> RPC RPC -->|HTTP/HTTPS| Odoo ``` ## MCP 核心概念 ### Resources vs Tools | 特性 | Resources | Tools | |------|-----------|-------| | **用途** | 提供上下文資訊 | 執行操作/動作 | | **觸發** | 客戶端控制(如 Claude Code) | LLM 自動決定呼叫 | | **參數** | 無(或 URI 參數) | 有(需 LLM 生成) | | **類比** | 員工手冊(背景知識) | 工具箱(按需使用) | | **HTTP 類比** | GET(讀取) | POST/PUT/DELETE(操作) | **Resources** - 動態上下文,LLM 一開始就知道的背景資訊: ``` odoo://user → "我是誰" odoo://company → "我在哪間公司" odoo://models → "有哪些模型可用" ``` **Tools** - 需要時才呼叫的操作: ``` search_records(model="res.partner", domain=[...]) → 搜尋 create_record(model="sale.order", values={...}) → 建立 ``` ### 為什麼不用 Default Prompt? | 方式 | Default Prompt | Resource | |------|----------------|----------| | 資料來源 | 寫死在程式碼 | 即時從 Odoo 查詢 | | 更新時機 | 部署時 | 每次連線時 | | 換用戶登入 | 資訊錯誤 | 自動正確 | ```python # ❌ Default Prompt(寫死) SYSTEM_PROMPT = "當前用戶: Admin" # 換人登入就錯了 # ✅ Resource(動態) @mcp.resource("odoo://user") def get_current_user(): return client.read("res.users", [uid]) # 即時查詢 ``` **結論**:Resource 是「動態的上下文」,不是靜態文字。 > 參考:[MCP Resources](https://modelcontextprotocol.io/docs/concepts/resources) | [MCP Tools](https://modelcontextprotocol.io/docs/concepts/tools) ## 環境變數 | 變數 | 說明 | 預設值 | |------|------|--------| | `ODOO_URL` | Odoo 伺服器 URL | `http://localhost:8069` | | `ODOO_DATABASE` | 資料庫名稱 | - | | `ODOO_API_KEY` | API Key 認證 | - | | `READONLY_MODE` | 唯讀模式(禁止寫入操作) | `false` | | `MCP_ALLOW_SENSITIVE_MODELS` | 設 `true` 停用[模型黑名單](#模型黑名單mcp_allow_sensitive_models)(預設擋 credential 模型 `ir.config_parameter`、`res.users.apikeys` 的讀寫) | `false` | | `MCP_AUTH_TOKEN` | HTTP/SSE 模式的 Bearer Token 認證(未設定=無認證;stdio 不適用),見[安全機制](#安全機制) | -(停用) | | `MCP_MULTIUSER` | 多 user 模式:每個 client 拿**自己的 Odoo API key** 當 Bearer token,見[安全機制](#安全機制) | `false` | | `UPLOAD_TOKEN_SECRET` | upload token 的 HMAC secret;僅多 worker 部署需要設定(單一程序自動衍生) | - | 建立 `.env` 檔案: ```bash cp .env.example .env ``` ## 安裝 ```bash pip install -r requirements.txt ``` ## 啟動方式 ### 開發模式(MCP Inspector) ```bash fastmcp dev inspector odoo_mcp_server.py ``` ## 傳輸模式(Transport) 本專案支援三種 MCP 傳輸模式: | 模式 | 說明 | 適用情境 | |------|------|----------| | `stdio` | 標準輸入輸出(預設) | Claude Desktop、Cursor IDE、本機開發 | | `http` | HTTP 協定 | 遠端服務、n8n、Web 應用整合 | | `sse` | Server-Sent Events(已棄用) | 向下相容舊版 Client | ### stdio vs HTTP/SSE:算力位置 兩種模式的關鍵差異在於「誰來啟動 MCP Server」以及「算力在哪裡執行」: **stdio 模式(本機算力)** ``` ┌─────────────────────────────────────┐ │ 你的電腦 💻 │ │ │ │ Claude Desktop ──> MCP Server │ │ (使用本機算力) │ └─────────────────────────────────────┘ ``` - Client(如 Claude Desktop)啟動 MCP Server 作為子進程 - MCP Server 使用你電腦的 CPU/RAM - Server 隨 Client 啟動/關閉 **HTTP/SSE 模式(遠端算力)** ``` ┌──────────────┐ ┌──────────────────┐ │ 你的電腦 │ │ 雲端 ☁️ │ │ │ │ │ │Claude Desktop│ ──網路──>│ MCP Server │ │ (輕量) │ │ (使用雲端算力) │ └──────────────┘ └──────────────────┘ ``` - MCP Server 獨立運行在雲端/遠端主機 - 多個 Client 可同時連線同一個 Server - 適合團隊共用、n8n 整合、正式環境 ### 啟動不同模式 ```bash # stdio 模式(預設) python odoo_mcp_server.py # HTTP 模式 python odoo_mcp_server.py --transport http --host 0.0.0.0 --port 8000 # SSE 模式(已棄用,建議使用 HTTP) python odoo_mcp_server.py --transport sse --host 0.0.0.0 --port 8000 ``` ### 雲端部署(HTTP 模式) > ⚠️ **安全提醒**:HTTP 模式預設**沒有認證**——任何連得到該 port 的人都直接繼承 > `ODOO_API_KEY` 的完整權限。除非 server 只在受信任的內網使用, > 否則請務必設定 `MCP_AUTH_TOKEN`(或改用多 user 模式 `MCP_MULTIUSER`)並搭配 TLS,詳見[安全機制](#安全機制) 專案提供 `docker-compose.example.yml` 範本,複製後修改即可使用: ```bash cp .env.example .env # 填入 ODOO_URL / ODOO_DATABASE / ODOO_API_KEY cp docker-compose.example.yml docker-compose.yml # 依需求調整 docker compose up -d ``` 範本內容 ```yaml volumes: shared-uploads: services: odoo-mcp: build: . command: ["python", "odoo_mcp_server.py", "--transport", "http", "--host", "0.0.0.0", "--port", "8000"] # 對外暴露 port 8000(host 端 client 可直接連 http://localhost:8000/mcp)。 # ⚠️ "8000:8000" 會綁定 0.0.0.0:同網段的所有機器都連得到 # (主機若有公網 IP,就是整個網際網路),且 Docker 發佈的 port 會繞過 ufw 防火牆規則。 # 建議設定 MCP_AUTH_TOKEN(見 environment);只給本機 client 用可改 "127.0.0.1:8000:8000"。 # 若只需 Docker 內網存取(例如 client 也在同一個 compose 裡),可整段移除 ports。 ports: - "8000:8000" environment: - ODOO_URL=${ODOO_URL} - ODOO_DATABASE=${ODOO_DATABASE} - ODOO_API_KEY=${ODOO_API_KEY} - READONLY_MODE=${READONLY_MODE:-false} # HTTP 模式的 Bearer Token 認證(未設定=無認證,見 README「安全機制」) - MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN:-} # 多 user 模式:每個 client 拿自己的 Odoo API key 當 Bearer token, # 權限/審計歸屬真實 user(見「多 user 模式」) # - MCP_MULTIUSER=true # HTTP 模式的 Host 標頭防護(DNS rebinding protection,來自底層 MCP SDK): # 用非 localhost 的 IP/網域連進來時,預設會被擋下並回 "Invalid host header"。 # ⚠️ 快速測試可先全開(勿用於正式環境): - FASTMCP_HTTP_ALLOWED_HOSTS=["*"] volumes: - shared-uploads:/shared # 圖片傳遞通道;對應 Dockerfile 預建的 /shared/uploads restart: unless-stopped ``` > **圖片 / 附件傳遞**:`add_attachment` 的 `file_path` 模式會從 `/shared/uploads/` 讀檔上傳到 Odoo,避免大量 base64 佔用 LLM output token。client 與 server **跨機器**(不共用此 volume)時,改走 `prepare_upload` → `/upload` 把檔案送進 `UPLOAD_DIR`,詳見[安全機制](#安全機制)。 > **連不上、回 `Invalid host header`?** 這是底層 MCP SDK 的 **DNS rebinding 防護**——用非 localhost 的 IP/網域連進來時,Host 標頭不在允許清單內就會被擋。用 `FASTMCP_HTTP_ALLOWED_HOSTS` 放行: > > ```bash > # 快速測試(⚠️ 對任何 Host 開放,勿用於正式環境) > FASTMCP_HTTP_ALLOWED_HOSTS=["*"] > > # ✅ 正規做法:只列出 client 實際連線的 host(含 port) > FASTMCP_HTTP_ALLOWED_HOSTS=["your-server-ip:8000"] # 純 IP 部署 > FASTMCP_HTTP_ALLOWED_HOSTS=["mcp.example.com"] # 反向代理/網域(建議搭配 TLS) > ``` > > 官方明確警告:使用萬用字元 `*` 會讓 server 對任何來源開放,正式環境請務必列出明確 host。必要時另有 `FASTMCP_HTTP_ALLOWED_ORIGINS`(瀏覽器型 client 的 Origin 白名單)。 ```sh # server 有設 MCP_AUTH_TOKEN 時,需帶 Authorization header claude mcp add --transport http odoo-mcp https://your-cloud-server.com:8000/mcp --header "Authorization: Bearer your_random_token_here" # 多 user 模式(MCP_MULTIUSER=true):Bearer 改填「自己的 Odoo API key」 claude mcp add --transport http odoo-mcp https://your-cloud-server.com:8000/mcp --header "Authorization: Bearer <你的 Odoo API key>" # server 未啟用認證(僅限受信任內網) claude mcp add --transport http odoo-mcp https://your-cloud-server.com:8000/mcp ```
手動設定 JSON(加到 `~/.claude.json`) ```json { "mcpServers": { "odoo-mcp": { "type": "http", "url": "https://your-cloud-server.com:8000/mcp", "headers": { "Authorization": "Bearer your_random_token_here" } } } } ``` > ⚠️ 純 HTTP 下 Bearer token 是明文傳輸,僅適合受信任內網/臨時測試;對外請改用 `https://`(TLS)。 server 未啟用 `MCP_AUTH_TOKEN`、也未開多 user 模式時,`headers` 整段可省略。 多 user 模式(`MCP_MULTIUSER=true`)下 `headers` **必填**,Bearer 改填**自己的 Odoo API key**,見[多 user 模式](#多-user-模式mcp_multiuser)。
## MCP Resources | URI | 說明 | |-----|------| | `odoo://models` | 列出所有模型 | | `odoo://model/{model_name}` | 取得模型欄位定義 | | `odoo://record/{model_name}/{record_id}` | 取得單筆記錄 | | `odoo://user` | 當前登入用戶資訊 | | `odoo://company` | 當前用戶所屬公司資訊 | ## MCP Tools | Tool | 說明 | 唯讀 | |------|------|------| | `list_models` | 列出/搜尋可用模型 | Yes | | `get_fields` | 取得模型欄位定義 | Yes | | `search_records` | 搜尋記錄 | Yes | | `count_records` | 計數記錄 | Yes | | `read_records` | 讀取指定 ID 記錄 | Yes | | `create_record` | 建立記錄 | No | | `update_record` | 更新記錄 | No | | `delete_record` | 刪除記錄(需二次確認) | No | | `execute_method` | 執行任意模型方法(萬用入口,`unlink` 與黑名單模型已封鎖,見[安全機制](#安全機制)) | No | | `add_attachment` | 上傳附件到 Odoo(`file_path` / `base64_data` 兩種模式,見[安全機制](#安全機制)) | No | | `prepare_upload` | 取得 `/upload` 端點用法與短效 `upload_token`(跨機器傳檔,見[安全機制](#安全機制)) | No | ## Docker 建置 部分 client 的 Docker 設定(Claude Code / Gemini 的 Docker 版本)需要先建置本機映像檔: ```bash docker build -t odoo-mcp-server . ``` ## MCP Client 設定 本專案支援以下 MCP Client,各自的完整設定步驟見對應章節: | Client | 加入方式 | 設定檔 | |--------|----------|--------| | [Claude Code](#claude-code) | `claude mcp add` | `~/.claude.json` | | [Gemini CLI](#gemini-cli) | `gemini mcp add` | `~/.gemini/settings.json` | | [Antigravity CLI](#antigravity-cli) | 手動編輯 | `~/.gemini/config/mcp_config.json` | | [OpenClaw](#openclaw) | `openclaw mcp set` | OpenClaw config | | [Codex CLI](#codex-cli) | `codex mcp add` + 手動編輯 | `~/.codex/config.toml` | ### Claude Code 設定檔位於 `~/.claude.json`: #### 本機執行 ```sh claude mcp add odoo-mcp-server -- python odoo_mcp_server.py ```
手動設定 JSON ```json { "mcpServers": { "odoo-mcp-server": { "command": "/bin/python", "args": [ "odoo_mcp_server.py" ] } } } ```
#### Docker(host.docker.internal) 適用於 Odoo 執行在本機的情況: ```sh claude mcp add odoo-mcp-server -- docker run -i --rm --add-host=host.docker.internal:host-gateway -e ODOO_URL=http://host.docker.internal:8069 -e ODOO_DATABASE=odoo19 -e ODOO_API_KEY=your_api_key_here odoo-mcp-server ```
手動設定 JSON ```json { "mcpServers": { "odoo-mcp-server": { "command": "docker", "args": [ "run", "-i", "--rm", "--add-host=host.docker.internal:host-gateway", "-e", "ODOO_URL=http://host.docker.internal:8069", "-e", "ODOO_DATABASE=odoo19", "-e", "ODOO_API_KEY=your_api_key_here", "odoo-mcp-server" ] } } } ```
#### Docker(host network) 使用主機網路模式: ```sh claude mcp add odoo-mcp-server -- docker run -i --rm --network host -e ODOO_URL=http://localhost:8069 -e ODOO_DATABASE=odoo19 -e ODOO_API_KEY=your_api_key_here odoo-mcp-server ```
手動設定 JSON ```json { "mcpServers": { "odoo-mcp-server": { "command": "docker", "args": [ "run", "-i", "--rm", "--network", "host", "-e", "ODOO_URL=http://localhost:8069", "-e", "ODOO_DATABASE=odoo19", "-e", "ODOO_API_KEY=your_api_key_here", "odoo-mcp-server" ] } } } ```
#### Docker(遠端 Odoo) ```sh claude mcp add odoo-mcp-server -- docker run -i --rm -e ODOO_URL=https://example.com/ -e ODOO_DATABASE=odoo19 -e ODOO_API_KEY=your_api_key_here odoo-mcp-server ```
手動設定 JSON ```json { "mcpServers": { "odoo-mcp-server": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "ODOO_URL=https://example.com/", "-e", "ODOO_DATABASE=odoo19", "-e", "ODOO_API_KEY=your_api_key_here", "odoo-mcp-server" ] } } } ```
### Gemini CLI ```sh gemini mcp add --scope user odoo-mcp docker -- run -i --rm --add-host=host.docker.internal:host-gateway -e ODOO_URL=http://host.docker.internal:8069 -e ODOO_DATABASE=odoo19 -e ODOO_API_KEY=your_api_key_here odoo-mcp-server ```
手動設定 JSON(加到 `~/.gemini/settings.json`) ```json { "mcpServers": { "odoo-mcp": { "command": "docker", "args": [ "run", "-i", "--rm", "--add-host=host.docker.internal:host-gateway", "-e", "ODOO_URL=http://host.docker.internal:8069", "-e", "ODOO_DATABASE=odoo19", "-e", "ODOO_API_KEY=your_api_key_here", "odoo-mcp-server" ] } } } ```
### Antigravity CLI > 自 2026/6/18 起個人版 Gemini CLI 停止服務,改用 [Antigravity CLI](https://antigravity.google/)。目前 **沒有** `mcp add` 子指令,需手動編輯設定檔。 設定檔路徑為 `~/.gemini/config/mcp_config.json`(Antigravity CLI / IDE / SDK 共用,等同 Gemini CLI 的 `--scope user`)。 JSON 格式與上方 Gemini CLI 設定相同。 設定後進入 Antigravity CLI 以 `/mcp` 指令重新載入,並確認連線狀態。 ### OpenClaw OpenClaw 透過 CLI 管理 MCP server,設定會寫入 `mcp.servers.`。 > `/mcp` 指令為 **owner-only 且預設關閉**,需以 `commands.mcp: true` 開啟才能在 chat session 中使用。 #### 步驟 1:註冊 MCP server ```sh # 請將 your-server-ip 換成你的 MCP server 位址 openclaw mcp set odoo-mcp '{"type":"http","url":"http://your-server-ip:8000/mcp"}' ```
手動設定 JSON(寫入 OpenClaw 設定的 `mcp.servers`) OpenClaw 會自動正規化設定,把 `type:"http"` 轉成 `transport:"streamable-http"` 後存入: ```json { "mcp": { "servers": { "odoo-mcp": { "url": "http://your-server-ip:8000/mcp", "transport": "streamable-http" } } } } ```
#### 步驟 2:開啟 `/mcp` 指令 ```sh openclaw config set commands.mcp true ``` #### 步驟 3:重啟 Gateway 套用設定 ```sh openclaw gateway restart ``` > 若想等進行中的工作排空再重啟,可改用 `openclaw gateway restart --safe`。 #### 驗證 ```sh # server 是否註冊成功 openclaw mcp list openclaw mcp show odoo-mcp # /mcp 開關狀態(應回傳 true) openclaw config get commands.mcp ``` 完成後請**開一個新的 chat session(或硬重整 dashboard),再輸入 `/mcp`** 確認 `odoo-mcp` 連線狀態。 ### Codex CLI > Codex 的 `codex mcp add` **只支援 stdio(`command` / `args`)**,並不支援 url(streamable HTTP)形式的遠端 server。因此要連雲端 HTTP 模式的 MCP server,需先用佔位指令建立設定,再手動編輯 `~/.codex/config.toml`。 ```sh codex mcp add odoo-mcp -- echo placeholder ```
手動設定 TOML(修改 `~/.codex/config.toml`) `codex mcp add` 產生的佔位設定: ```toml [mcp_servers.odoo-mcp] command = "echo" args = ["placeholder"] ``` 手動改為 url(streamable HTTP): ```toml [mcp_servers.odoo-mcp] url = "https://your-cloud-server.com:8000/mcp" ``` > 若 server 端設定了 `MCP_AUTH_TOKEN`,需加上 `bearer_token_env_var = "ODOO_MCP_TOKEN"`, > 並在執行 Codex 的環境中 `export ODOO_MCP_TOKEN=<與 server MCP_AUTH_TOKEN 相同的值>`; > 或改用自訂 `http_headers` 直接填 `Authorization` header。 > 多 user 模式(`MCP_MULTIUSER=true`)下同理,`ODOO_MCP_TOKEN` 改 export **自己的 Odoo API key**。
## 安全機制 ### 部署定位與 HTTP 認證(`MCP_AUTH_TOKEN`) 設定 `MCP_AUTH_TOKEN` 環境變數即可啟用 Bearer Token 認證(opt-in): ```bash # 產生隨機 token openssl rand -hex 32 ``` - 啟用後 `/mcp` 端點要求 `Authorization: Bearer `,未帶或錯誤一律回 401 - 未設定時行為與過去版本相同(無認證),但 HTTP/SSE 模式啟動時會在 stderr 印出警告 ### 多 user 模式(`MCP_MULTIUSER`) 預設情況下,所有操作都透過 `ODOO_API_KEY` 這一個 Odoo 帳號執行——多人共用時, Odoo 端的權限、chatter、審計紀錄全部歸到同一個 user。設定 `MCP_MULTIUSER=true` 後改為 **pass-through 認證**: - 每個 user 在 Odoo「偏好設定 → 帳戶安全」產生自己的 API key,直接當 HTTP/SSE 連線的 Bearer token(json2 protocol 的 API key 即完整憑證, Odoo 會從 key 解析出擁有者) - Server 收到 token 後拿去問 Odoo 驗證(結果快取 5 分鐘、驗證失敗快取 30 秒 以防暴力破解),驗證成功即建立**綁該 user API key 的專屬連線** - 權限(ACL / record rules)、操作歸屬、審計全部由 Odoo 原生機制處理; 撤銷存取 = 在 Odoo 刪除該 API key(最遲 5 分鐘後生效) - Server 端**不保存任何使用者資料**,新增/移除 user 都在 Odoo 操作即可 - `MCP_AUTH_TOKEN` 可並存,作為 admin fallback(走 `ODOO_API_KEY` 的共享連線); stdio 模式不受影響 - 純多 user 部署(不設 `MCP_AUTH_TOKEN`、不跑 stdio)可**連 `ODOO_API_KEY` 都不設** ——server 端零長效憑證,設定檔外洩也沒東西可偷。誤走到 fallback 路徑時會收到 明確的設定錯誤訊息(不會拿 placeholder 去打 Odoo) - Bearer token 就是 Odoo API key 本體,**務必搭配 TLS** Client 端設定與 `MCP_AUTH_TOKEN` 完全相同(完整範例見[雲端部署(HTTP 模式)](#雲端部署http-模式)), 只是 Bearer 換成**各自的 Odoo API key**: ```json "headers": { "Authorization": "Bearer <你的 Odoo API key>" } ``` ### 附件檔案讀取範圍(`UPLOAD_DIR`) `add_attachment` 的 `file_path` 模式是本 server 唯一會讀取 MCP 主機本地檔案的入口。 若不設限,被 prompt injection 的 LLM 可用 `file_path="/app/.env"` 把 server 機密 (含 `ODOO_API_KEY` 本身)讀出、上傳成 Odoo 附件外洩——這是 confused deputy, `MCP_AUTH_TOKEN` 擋不住(LLM 本來就是合法持 token 的 client)。 因此 `file_path` 被限制在 `UPLOAD_DIR`(預設 `/shared/uploads`)底下: - 路徑經 `Path.resolve()` 正規化後,必須落在 `UPLOAD_DIR` 內,否則回 `ToolError` - `resolve()` 會一併解掉 symlink,所以「白名單目錄裡放一個指向外部的 symlink」也擋得掉 - 預設值對齊 compose 的 `/shared/uploads` 圖片傳遞通道,Docker 部署無需額外設定 - 純本機 stdio 若要放行任意路徑,設 `UPLOAD_DIR=/`(等於解除限制,自負風險) - 檔案不在磁碟上(如 Discord 上傳的圖片)時,改用 `base64_data` 模式,不受此限制 ### 跨機器上傳圖片(`prepare_upload` → `/upload`) 當 client 與 server **不在同一台機器**時,`shared-uploads` volume 用不到,`file_path` 沒有共用檔案系統可讀;若改走 `base64_data`,整包 base64 會流經 LLM 的 token stream,又慢又貴。 `/upload` 提供一條 out-of-band 的檔案通道:client 用普通 HTTP POST 把位元組直接推到 server (不經 LLM),server 存進 `UPLOAD_DIR` 後回傳 `file_path`,client 再用這個路徑呼叫 `add_attachment`——只有短路徑字串會進 token stream。 整個工作流透過 MCP 協定自我描述,client 端**零安裝、零設定**:agent(如 Claude Code)呼叫 `prepare_upload` 工具就拿到端點用法與短效 `upload_token`,接著自己上傳: ```sh # upload_token 由 prepare_upload 簽發(server 未設 MCP_AUTH_TOKEN 時免帶 header) curl -fsS -F "file=@/local/invoice.png" \ -H "Authorization: Bearer " \ https://your-server:8000/upload # → {"file_path": "/shared/uploads/.png", "file_name": "invoice.png"} ``` 接著呼叫 `add_attachment(file_path="/shared/uploads/.png", file_name="invoice.png", ...)`。 不經 MCP 的手動整合(腳本、CI 等)也可以直接拿 `MCP_AUTH_TOKEN` 本體打同一個端點。 安全機制: - `/upload` 是**會寫檔**的端點,設了 `MCP_AUTH_TOKEN` 就要求 Bearer token(custom route 不受 MCP 認證保護,故自行驗證) - `prepare_upload` 簽發的是 **HMAC 衍生短效 token**(`MCP_AUTH_TOKEN` 為根秘密簽出、預設 10 分鐘、只對 `/upload` 有效、無狀態驗證)——master token 不進 LLM context,就算對話 transcript 外流,外洩的也只是效期內的上傳權限 - 磁碟檔名由 server 端 `uuid` 產生,client 給的檔名**絕不進入路徑**,無法逃出 `UPLOAD_DIR` - 單檔大小上限 `UPLOAD_MAX_BYTES`(預設 25 MiB),超過回 413 - 不自動清理 `UPLOAD_DIR`,請搭配定期清理或使用 ephemeral volume ### 模型黑名單(`MCP_ALLOW_SENSITIVE_MODELS`) 預設封鎖以下兩個機密模型的**所有讀寫**,防範 prompt injection——LLM 讀到藏在資料裡的惡意指令,拿著你的 API key 做出「權限上合法、但你沒要求」的操作: - `ir.config_parameter`:存第三方 API key、webhook token 等機密,一句 search 就全外洩 - `res.users.apikeys`:可產生新的長效 API key——事後 rotate 原 key 也擋不住,等於留後門 正常的 agent 工作流程用不到這兩個模型,封鎖不影響日常使用;所有工具與 Resources(含 `execute_method`)都會檢查。 > 黑名單只是多一層保險,**不是權限控管**——權限仍由 Odoo ACL 決定,請給 MCP 低權限使用者的 API key > (`res.users`、`ir.rule` 等安全模型因此不在黑名單內)。 > 真的需要透過 MCP 管理這兩個模型時,設 `MCP_ALLOW_SENSITIVE_MODELS=true` 停用。 ### 唯讀模式 設定 `READONLY_MODE=true` 啟用唯讀模式,適用於生產環境查詢: - 寫入工具(`create_record`、`update_record`、`delete_record`、`execute_method`、`add_attachment`)在註冊時即被停用——LLM 看不到這些工具,直接呼叫也會被拒絕 - 停用發生在模組層級,任何啟動方式(`python odoo_mcp_server.py`、`fastmcp run`、`fastmcp dev`)都同樣生效 ### 刪除二次確認與 `execute_method` 的安全邊界 `delete_record` 內建 confirm 機制:LLM 必須先以 `confirm=False` 呼叫取得確認提示,經使用者同意後才能以 `confirm=True` 執行刪除。 > 注意:confirm 參數由 LLM 自行填入,屬於「引導 LLM」層級的防護, > 並非強制性的安全邊界(LLM 理論上可直接傳 `confirm=True`)。 `execute_method` 是萬用入口(escape hatch),用來呼叫沒有專用工具的模型方法,請理解其風險: - ORM 原語 `unlink` 已被攔下,會導向 `delete_record` 的二次確認流程,無法藉此繞過確認 - [模型黑名單](#模型黑名單mcp_allow_sensitive_models)同樣適用:credential 模型(`ir.config_parameter`、`res.users.apikeys`)全擋 - 但 `action_confirm`、`action_post`、`button_validate` 這類會改資料的**業務方法不設防**—— Odoo 有上千個模型方法,server 無法枚舉哪些會寫入資料庫 - 真正的安全邊界應該是給 MCP 使用**低權限的 Odoo 使用者** API key(最小權限原則) ## 健康檢查 HTTP/SSE transport 模式下提供 `/health` 端點: ```bash curl http://localhost:8000/health # {"status": "healthy", "service": "odoo-mcp-server", "version": "1.2.0"} ``` 適用於 Docker healthcheck、Kubernetes probe、load balancer 探活。stdio 模式下不影響。 此端點**不受** `MCP_AUTH_TOKEN` 保護(不需帶 token)。 ## License Apache 2.0