跳转至

架构

面向好奇者和贡献者。权威来源是仓库中的 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 协议(不使用 mcp SDK 或其他依赖):以换行分隔的 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 通道使用。

一次工具调用中的检查顺序

  1. check_readonly(未变):在 ODOO_READONLY=1 下,无论参数如何,写入工具都会被拒绝。
  2. Policy.check_tool:被锁定的策略、没有映射的工具、被禁用的工具,或配置档在所有模型上都禁止的工具都会被拒绝。这些就是 tools/list 隐藏的工具。
  3. 参数按工具的输入模式进行校验。
  4. A 层(Policy.check):声明的触及项、名称检查、NEVER、被屏蔽的字段名、ir.attachment 的固定、内部备注规则和上下文键,全部根据参数判断。在这里被拒绝时从不登录,也不计为一次工具调用。
  5. 处理器在 GuardedSession(B 层)下运行:每个 Odoo 调用都会针对实际的模型和方法,连同其值和路径(x2many 命令、字段路径、附件、身份、委托链接,以及在 Odoo 10 至 12 上用模型字段判定的按位置传递对象的键)再检查一次,对照工具声明的触及项或它的某个内部调用及其谓词,然后才会触及传输层。读取结果会丢掉被屏蔽的字段(policy_withheld_fields)。
  6. 无论结果如何,审计都会为这次调用写一行。

传输方式与模式

  • 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)永不阻塞启动。
  • 日志和磁盘中没有任何密钥。