架构¶
面向好奇者和贡献者。权威来源是仓库中的
docs/architecture.md。
v1.6.0 新增
install_check.py 会点名作出响应的 odoo-tools 副本(odoo_version 的 plugin 块),并离线运行副本检查和选项检查(run_stdio.py --check-copies 和 --check-options)。在 Odoo 10 至 12 上,B 层会用模型的字段来判定按位置传递给业务方法的对象中的键。
v1.5.1 新增
diagnostics.py 写入只含元数据的本地诊断(server.jsonl),run_stdio.py --diagnose 在不联系 Odoo 的情况下打印支持报告。A 层,以及在 Odoo 10 至 12 上的 B 层,都会把按位置传递的对象当作上下文来读取。服务器不再读取 ODOO_HTTP_BEARER。
v1.5.0 新增
权限策略 在会话存在之前对每次调用做出决定(A 层,policy/check.py),一个受守卫的会话会检查工具发出的每个 Odoo 调用(B 层,policy/guard.py)。audit.py 为每次调用写一行。基于请求头的多租户路径已被移除。
Claude (MCP 客户端)
│ stdio (JSON-RPC MCP)
▼
odoo_mcp.server ──列出/派发──▶ registry (ToolRegistry,每个工具的 Access)
│ policy/check.py:A 层, │
│ 在任何会话之前 ▼
│ tools/ (crud, meta, export, i18n,
│ report, studio, online)
│ │ resolve_model / resolve_fields
│ ▼
│ compat/ (detect → deltas → resolve)
│ │ ctx.session
▼ ▼
tenancy.ConnectionManager ──▶ policy/guard.GuardedSession(B 层:每个 Odoo 调用)
(按租户) │
▼
session.OdooSession ──▶ transport/
(认证, facts, 缓存) fallback → json2 | jsonrpc | xmlrpc
│
▼
Odoo 实例
audit.py:每次工具调用一行(包括被拒绝的调用),写入本地文件或 stderr
层次¶
- transport/:
Json2Transport(v1.4.0 新增:使用 bearer API key 的POST /json/2/<模型>/<方法>;位置参数用json2_signatures.py命名,其他方法用/doc-bearer命名,当 Odoo 的校验拒绝某个模型重写的参数名称时会修正一次)、JsonRpcTransport、XmlRpcTransport和FallbackTransport,后者根据未约束的版本系列、凭据类型以及 URL 中的user:pass@来选择顺序(auto:在有 API key 的 saas~18.4 / 19.0+ 上为 JSON-2 → JSON-RPC → XML-RPC,其他情况下为 JSON-RPC → XML-RPC;自 saas~21.1 起仅 JSON-2)。它在检测版本时既不登录也不使用已弃用的端点,在一次失败后固定使用能响应的传输方式(JSON-2 则在其第一次登录后固定),并提示一次。写操作(任何不是读取方法的调用)只有在请求确定没有到达 Odoo 时才会转到下一种传输方式;超时、HTTP 5xx 或无法解析的响应之后会直接返回错误("may or may not have been applied")。读取操作仍会通过下一种传输方式重试。HTTP 429 为RateLimited:读取在同一传输方式上等待Retry-After,写入则会被报告。在 Odoo 19+ 上使用经典传输方式时,会提供transport_notice。它们都只使用标准库(urllib和xmlrpc.client),共享一个会验证证书的 TLS 上下文,并遵循ODOO_TIMEOUT。 - session.py:
OdooSession保存凭据、以惰性方式认证(uid)、缓存版本/版本类型/部署方式的 facts,并将模式读取包裹在 TTL 缓存SchemaCache中。ConnectionManager(tenancy.py)将一个会话键(Credentials.session_key():url|db|login、凭据类型以及按进程计算的密钥 HMAC,从不 包含密钥本身)映射到一个会话。 - compat/:
detect.probe()构建EnvFacts;deltas.py是声明式的 deltas 映射表;resolve.py将请求的模型/字段/能力转换为目标中实际存在的内容。每个工具都在调用 ORM 之前先调用resolve_*。 - tools/:注册在共享
registry上的轻量处理器。写入工具带有read_only=False:ODOO_READONLY=1会在server.py中隐藏它们,并在会话存在之前就拒绝它们。每个工具还会声明它触及什么(access=),供权限策略使用。公开核心有一套声明式的权限策略;审批流程不在其中(那属于一个私有的治理层)。tools/online.py包含 Odoo Online 工具。其中两个导入工具把调用限制在 JSON-2 和 JSON-RPC 上(transports=("json2", "jsonrpc")),回退 机制绝不会通过 XML-RPC 发送或重放它们,因为 XML-RPC 的序列化器无法返回导入结果中携带的None值。session.api_doc(model)提供/doc-bearer,而不把密钥交给任何工具。 - policy/:权限策略:层级数据(
tiers.py)、方法类别和参数形态(classify.py)、配置档、规则和Policy.decide()(model.py)、读取选项和文件并给出 E 代码与警告的加载器(loader.py)、A 层(check.py)、B 层的受守卫会话(guard.py)、智能体看到的文本(texts.py)以及--check-policy(cli.py)。除守卫之外全部是纯逻辑:不调用 Odoo,也没有 I/O,只在启动时读取一次策略文件。 - audit.py:每次工具调用(包括被拒绝的调用)一行以哈希链接的 JSON,只包含名称和计数,写入仅你的用户可访问的每日文件(
ODOO_AUDIT_DIR)、stderr,或不写入(off)。--verify-audit 文件检查一个文件的链。 - diagnostics.py(自 v1.5.1 起):本地诊断。服务器的每个事件(
start、connect、crash、stop)在ODOO_DIAG_DIR文件夹中的server.jsonl里写一行只含元数据的 JSON;每个键都要经过允许清单,因此它从不保存 URL、数据库、登录名、凭据、策略文件路径、记录数据、参数或错误文本。每行都小于 2 KB,每个文件在 256 KiB 时轮换,写入失败也从不影响工具调用。--diagnose读取这些文件和审计日志,从不联系 Odoo。 - install_check.py(自 v1.6.0 起):安装的是哪个 odoo-tools 副本,以及它的选项是否完整。
plugin_identity从 Claude Code 为每个副本分配的数据文件夹名称(~/.claude/plugins/data/<名称>-<marketplace>)中读出副本,odoo_version在plugin中报告它。--check-copies从标准输入读取claude plugin list --json的输出,--check-options读取claude plugin configure <id> --json的输出:它们不运行任何程序、不打开 Claude Code 的任何文件,从不联系 Odoo,也从不打印敏感值,并以 0(无需修正)、1(需要修正)或 2(输入不是那种 JSON)退出。 - observability.py:可选的 Prometheus 指标和 OTLP 追踪,两者默认均关闭,且在附加项未安装时为 no-ops。
- server.py / main.py / run_stdio.py:基于 Python 标准库实现的 MCP
stdio 协议(不使用
mcpSDK 或其他依赖):以换行分隔的 JSON-RPC 2.0,支持initialize(协商协议版本:2025-06-18、2025-03-26或2024-11-05)、ping、tools/list(带annotations.readOnlyHint)、tools/call(工具错误以带isError的结果返回)、initialized与cancelled通知,以及批量请求(batch)。工具调用在 单个工作线程中按到达顺序 执行:在 Odoo 处理期间,读取端仍会响应ping并处理取消请求,且会话/传输永远不会被并发使用。run_stdio.py是插件启动的入口点(python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py);在独立模式下仍可使用odoo-mcp/python -m odoo_mcp,它们也能运行离线检查--check-policy 文件、--verify-audit 文件、--diagnose(支持报告)、--check-copies和--check-options。日志输出到 stderr,任何零散的print()也会被重定向到那里;stdout 专供 MCP 通道使用。
一次工具调用中的检查顺序¶
check_readonly(未变):在ODOO_READONLY=1下,无论参数如何,写入工具都会被拒绝。Policy.check_tool:被锁定的策略、没有映射的工具、被禁用的工具,或配置档在所有模型上都禁止的工具都会被拒绝。这些就是tools/list隐藏的工具。- 参数按工具的输入模式进行校验。
- A 层(
Policy.check):声明的触及项、名称检查、NEVER、被屏蔽的字段名、ir.attachment的固定、内部备注规则和上下文键,全部根据参数判断。在这里被拒绝时从不登录,也不计为一次工具调用。 - 处理器在
GuardedSession(B 层)下运行:每个 Odoo 调用都会针对实际的模型和方法,连同其值和路径(x2many 命令、字段路径、附件、身份、委托链接,以及在 Odoo 10 至 12 上用模型字段判定的按位置传递对象的键)再检查一次,对照工具声明的触及项或它的某个内部调用及其谓词,然后才会触及传输层。读取结果会丢掉被屏蔽的字段(policy_withheld_fields)。 - 无论结果如何,审计都会为这次调用写一行。
传输方式与模式¶
- stdio(默认): 单租户。这是插件的
mcpServers条目所用的模式,该条目会将插件选项中的凭据传给服务器进程;在独立模式下,凭据从环境中的ODOO_*变量读取。 - HTTP(路线图,尚未构建): 一种远程模式,由服务器保管 Odoo 凭据,并根据已认证的身份解析会话。它需要单独设计。基于请求头的
tenancy.from_headers(由客户端选择的X-Odoo-Url/Db/Login,以及一个原样传给 Odoo、并会回退到服务器自身密钥的Bearer)没有任何调用方,已在 v1.5.0 中移除;ODOO_MULTITENANT仍会被读取,但没有任何效果。过去被读取但从未使用的ODOO_HTTP_BEARER已在 v1.5.1 中移除:服务器不读取任何.claude-plugin/plugin.json没有从sensitive选项传入的凭据。
设计原则¶
- 跨版本的单一工具接口:调用方使用现代名称。
- 失败时给出补救建议:
CompatError会解释需要更改什么;不可用的字段会降级为警告,而非硬性失败。 - 零运行时依赖:仅使用 Python 3.9+ 标准库;可选依赖(cache/metrics/otel)永不阻塞启动。
- 日志和磁盘中没有任何密钥。