Skip to content

v0.8.0

Latest

Choose a tag to compare

@ivnvxd ivnvxd released this 26 Aug 18:39
Immutable release. Only release title and notes can be modified.
4c28709

Added

  • get_current_context tool: returns the connected user, timezone, company scope and UTC guidance as structured data.
  • Personalized initialize instructions: the same context block is sent at handshake. When the user read fails it falls back to the UTC guidance plus a note naming the server's own reason (e.g. the caller is not in the MCP User group), or the likely cause when the server gives none.
  • get_fields tool: schema discovery with curated attributes by default; honors field_names and attributes.
  • Binary & attachment resources: odoo://{model}/record/{id}/{field} and odoo://attachment/{id}, served with per-read mimeTypes.
  • No inlined base64: populated binary fields are returned as fetchable resource URIs instead (reads use bin_size).
  • x2many previews: get_record returns display-name summaries for small collections in related_summaries.
  • Inverse-field drilldown hints: the record resource's one2many "view all" filters on the inverse field (generic res_id inverses and domain-restricted one2manys keep the id-in domain).
  • Offset depth cap: offsets are capped at 1000 pages of limit, floored at 10,000 rows.
  • aggregate_records pagination: reports has_more and next_hint.
  • aggregate_records overall totals: an omitted or empty groupby returns a single overall row.
  • post_message subject: optional subject argument.
  • Archived records: the record resource serves them (active_test=False), matching what get_record already returned.
  • all_fields_fallback: new field_selection_method value, reported when smart selection is unavailable and every field was read.

Security

  • Attachment reads gate on the attached-to model: res_model is checked alongside ir.attachment, so enabling that one model no longer exposes every attachment body on the database.
  • Attachment metadata is gated too: the search_records/get_record/aggregate_records tools and the record/search/count resources scope ir.attachment to res_models the caller may read — a row carries url and index_content (the extracted document text). An allowlist the module cannot report fails closed, as a retryable "could not verify access".
  • Attachment writes are gated too: create_record/update_record/delete_record and post_message's attachment_ids check the attached-to model. An ungated update_record could repoint an excluded model's attachment at an allowed one and then read it back, bypassing the read gate entirely.
  • run_http() host/port removed: it reassigned app.settings.host after FastMCP had already chosen transport security from config.host, so an embedder could bind loopback with DNS-rebinding protection left off; the bind now always follows config.host/config.port.
  • Internal endpoints in error text: hostnames, private IPs, ports and URLs are scrubbed from sanitized messages — a connection or DNS failure quotes the endpoint it tried, and the ERROR_MAPPINGS branch returned before the removal patterns ran.
  • ODOO_MCP_ALLOWED_HOSTS Origin ports: an entry that pins a port now pins it for Origin too; the wildcard :* origin trusted a page served from any other port on the same host.
  • ODOO_MCP_MAX_BINARY_SIZE (default 50 MB): bounds a single binary/attachment read, checked before the payload is fetched.
  • Sensitive-field filtering: credential-named fields are withheld from bulk reads with an explanatory note; explicitly named fields are still returned. Covers *password/*_pass/*secret/*_token, the *_api_key/*_hmac_key/*_signature_key compounds, OAuth refresh tokens, passkeys, salts, OTP secrets and PINs, including trailing-underscore spellings (pass_, api_key_).
  • Credential-shaped log keys: write-payload logging now uses the same detector as the read paths, so smtp_pass/webhook_secret are redacted instead of logged in cleartext.
  • call_model_method alias gaps: copy_data, copy_multi, update, get_view and get_views are refused alongside the primitives they alias.
  • Sanitized error mappings: captured fault text is scrubbed before interpolation — file paths, host:port pairs and memory addresses no longer reach the client through ERROR_MAPPINGS.
  • call_model_method hardening: rejects ir.actions.*/ir.cron, the web_* family, and ORM CRUD primitives; list results truncate at 100 items.

Fixed

  • Smart field selection: the non-stored score cap gated on a compute key fields_get() never returns; it now gates on store=False (related fields exempt).
  • Oversized record ids: ids outside the XML-RPC 32-bit range are rejected with a clean validation error before any RPC.
  • Oversized ints in domains, values and method arguments: domains, create_record/update_record values and call_model_method arguments are all range-checked, instead of an OverflowError surfacing as Connection error: Operation failed.
  • YOLO list_models cap: the silent 200-model cap is gone — listings truncate at 500 with an explicit note and the real total.
  • YOLO list_models filter: the system-model exclusion is prefix-anchored (! + =like); plain like matched substrings and hid every model merely containing ir./base. (repair.*, ...).
  • Fault classification: business faults route on Odoo's faultCode (2 = UserError/ValidationError, 4 = AccessError), the only signal /xmlrpc/2/* sends — previously every business error read as Connection error: Operation failed:.
  • Business error line structure: multi-line messages keep their newlines, and trailing CONTEXT:/HINT: lines are no longer stripped from business exceptions (psycopg2 diagnostics still are).
  • Traceback frame detection: a frame is matched by shape, so a business message quoting a filename at line start is no longer mistaken for one and truncated.
  • Unsanitized error paths: the record, search, count and fields resource handlers scrub the exception before surfacing it.
  • Non-loopback bind warning: the HTTP startup warning now also states that DNS-rebinding protection is off unless ODOO_MCP_ALLOWED_HOSTS is set.
  • ODOO_MCP_ALLOWED_HOSTS port-less authority: a port-less entry now also allows the bare Host/Origin a proxy sends on 80/443, which previously matched nothing.
  • ODOO_MCP_ALLOWED_HOSTS IPv6: bracketed and bare literals are parsed and normalized instead of producing junk patterns that locked the deployment out.
  • HTTP session teardown: keys off the transport that actually started, so run_http() under a default stdio config no longer strands tool handlers on a dead connection (#70).
  • aggregate_records groupby collision: aggregating a field that is also a groupby key is refused on the pre-19 path instead of returning corrupted groups.
  • aggregate_records duplicate aggregates: two aggregates over one field are refused on the pre-19 path, where read_group dropped one and mislabeled the other.
  • __extra_domain drilldown contract: documented — AND it with the caller's domain (group condition only on Odoo 19; already the full domain on older servers).
  • list_resource_templates: unreadable models are filtered out when /mcp/models reports per-model operations (best-effort — older modules omit the block and everything stays listed).
  • id: aggregates on Odoo 15/16: refused with an explanation — read_group deletes the id key before returning, so the value silently never arrived.
  • __extra_domain on overall totals: the key is always present for groupby=[], filled in as [] on Odoo 15/16, which omit it entirely (17/18 return the caller's domain, 19 a trivially-true condition — re-ANDing any of them is a no-op).
  • Unbalanced domains: a domain whose &/|/! operators lack operands is rejected with a clear message before any RPC, instead of reaching Odoo as a server-side "syntactically not correct".
  • Deeply nested domain/fields strings: rejected on their own nesting depth rather than on the parser failing, so a 2 KB [[[[...]]]] is an invalid parameter on every interpreter — CPython 3.12 raised the JSON scanner's recursion ceiling, making the same input a stack-exhausting success there and a RecursionError on 3.11. Depth is counted quote-aware, so bracket characters inside values do not trip it.
  • Mutated caller context dicts: search(), read() and search_read() copy the context they are given — execute_kw injects the locale into it, so a caller reusing one dict accumulated our keys.
  • Caller-supplied invalid lang: no longer blamed on ODOO_MCP_LOCALE — a bad context language used to null the configured locale for every later request in the process.
  • ModelInfo.operations description: was "Allowed operations (standard mode only)"; now states it is null in YOLO mode, where the flags are global and reported once under yolo_mode.operations.
  • Session cookie race: the REST session id is read under the lock that creates it, so a concurrent 401 handler can no longer produce Cookie: session_id=None.
  • Self-explaining access refusals were flattened: the sanitizer rewrote any message merely containing "access denied" to a bare Permission denied for this operation, so the module's actionable wording (e.g. a user outside the MCP User group) never reached the caller; only a bare refusal maps to the generic text now.
  • Doubled "Access denied: " prefix: refusals that already label themselves are no longer prefixed again across the tool and resource handlers.
  • list_models swallowed the reason: an access failure reported Failed to list models: Permission denied for this operation instead of what the server actually said.
  • Discarded 403 diagnostics: the MCP module's own message (Model 'x' is not enabled for MCP access.) is surfaced instead of a generic Access denied to MCP endpoints, which pointed at a credential problem that did not exist.

Changed

  • Documentation pass: corrected stale claims (mcp ≥1.27 floor, YOLO auth needing ODOO_USER, Odoo 17+ HTML escaping, handle_error contract), documented ODOO_MCP_LOG_FORMAT/ODOO_MCP_SLOW_OPERATION_THRESHOLD_MS, and consolidated duplicated comments.
  • list_models counts: total is the number of models returned and total_available the database count; both are emitted in every mode.
  • Stored binaries in resource reads: the record/search resources previously skipped every binary field; stored ones are now read under bin_size and rendered as fetchable URIs. Non-stored ones stay out — bin_size cannot short-circuit a compute (sale.order.tax_totals made a 10-row read 3.3x slower).
  • Default search resource reads: odoo://{model}/search reads only the fields its one-line summary renders, instead of every safe field of every record and then discarding them.
  • CI targets Odoo 19: both integration jobs run on odoo:19, and the MCP job now sources the module from much-GmbH/much-mcp-server@19.0 (was ivnvxd/odoo-apps@18.0) using the MCP_MODULE_PAT secret. The module's declared Python dependencies are installed into the Odoo image so mcp_server can install.
  • mcp floor raised to 1.27: session_idle_timeout is passed to StreamableHTTPSessionManager, which does not accept it before 1.27.

Removed

  • build_search_uri: unused public helper removed from uri_schema.
  • Cache.invalidate_pattern: orphaned when the record cache was removed.