OdooSurface MCP
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| ODOO_DB | Yes | Odoo database name | |
| ODOO_URL | Yes | URL of the Odoo instance (e.g., http://localhost:8069) | |
| ODOO_USER | Yes | Odoo username | |
| ODOO_PASSWORD | Yes | Odoo password or API key |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| list_skillsA | List all atomic skills (canonical recipes for ONE thing). Returns name, summary, hint, applies_to, and used_in (workflows that compose this skill). No body — call get_skill for full text. |
| get_skillsA | Return one or more skills in full: frontmatter + markdown body + used_in back-references. Pass a single name or multiple names in the array. Missing names return {name, error} in the same array (partial success). |
| find_skillB | Find skills matching a situation. Filters by model, field_type, and/or operation against each skill's applies_to. Returns full skill records (with body) for all matches; empty array if none. |
| list_workflowsA | List all multi-step workflows. Returns name, summary, applies_to, and the skills each workflow composes. No body — call get_workflow for full text. |
| get_workflowsA | Return one or more workflows in full: frontmatter + markdown body. Pass a single name or multiple names in the array. With expand_skills=true, each workflow also includes the full body of every skill it composes. Missing names return {name, error} in the same array (partial success). |
| get_modelsA | List primary models the user can navigate to via menus (get_models()), or list relational models reachable from a base model via its form-view fields (get_models({ base: "sale.order" })). |
| get_model_actionsB | Return all actions available on a model: server actions (Action menu), report actions (Print menu), and form-view buttons (type=object/action) with their invisible condition (Python expression, or a domain on Odoo 15/16). Also returns CRUD access flags for the current user. |
| get_model_interfaceA | Single-call planning helper: returns form-view field metadata AND all model actions (server actions, reports, view buttons) AND CRUD access flags. Use this before creating or editing records to understand the full model interface without separate get_fields + get_model_actions round trips. |
| get_available_actionsB | Return the buttons and actions that are actually visible for a specific record right now, based on its current field values. Mirrors what the Odoo web client shows when a user opens the form view: invisible conditions are evaluated with the web client's own evaluator against the record. Returns {visible_buttons[], server_actions[], reports[], can_create, can_write, can_delete}. |
| list_recordsA | Return a paginated list of records — as the list view or the Export dialog would show them. domain: Odoo domain, e.g. [["state","=","draft"]]; ANDed with the action's domain when action_id is given. fields: field names to return (any readable field; relational ones as [id, display_name]); default: the list view's columns. Pass context to control read behaviour — e.g. {lang: "fr_FR"} returns translated field values, {active_test: false} includes archived records. order: e.g. "date desc, id"; ordering by a many2one follows the related model's own order (e.g. order_id on sale.order = date_order desc, id desc), which can look like order being ignored. Returns {total, offset, limit, records[]}. |
| read_groupA | Count and aggregate records per group — as the list view grouped by a field shows them. groupby: field names, dates with a granularity ("create_date:month"; day|week|month|quarter|year); empty for one total. aggregates: "field:agg" specs (sum, avg, min, max, count_distinct, …). domain filters the records first. Returns [{: value, __count, : value}] — many2one values as [id, name], date groups as [range start, label]. |
| get_recordA | Return form-view field values for a single record. Pass fields to fetch a specific subset instead of all form-view fields. Pass context to control read behaviour — e.g. {lang: "fr_FR"} returns field values in that language for all translate=True fields on the record. |
| search_recordsA | Find records by name — "which record is called X" — with the model's own name matching (display name, plus per-model keys such as reference, email or code). domain / action_id narrow the search. For filtering by field values use list_records. Pass context for search-time behaviour — e.g. {active_test: false} finds archived records, {lang: "fr_FR"} matches and returns display_name in that language. Returns [{id, display_name}] up to limit. |
| get_fieldsA | Return metadata for all fields visible in a model's form or list view. view_type: "form" (default) or "list". Returns [{name, string, type, required, readonly, relation?, selection?}]. |
| get_defaultsA | Return the default field values Odoo would pre-fill when clicking New. Pass action_id to include the action's context (e.g. default_partner_id). Pass context dict directly for wizard models. |
| get_filtersA | Return saved filters and favourites available for a model's list view. These appear in the Filters and Favourites dropdown in the Odoo UI. |
| list_snippetsA | List available website building-block snippets. Optional "search" filters by any substring of the key or name (case-insensitive). Returns {available_modules: [], snippets: [{key, name, module}]}. Use get_snippet(key) to fetch the ready-to-inject HTML. |
| get_snippetA | Fetch the ready-to-inject HTML for a website building-block snippet. Pass the snippet key (e.g. "website.s_text_image"). Returns {key, name, html} or {error}. |
| list_attachmentsA | Search ir.attachment records for any model. Returns metadata only — never binary data. Use to find existing files before uploading duplicates. src is ready to use as an image/file URL. |
| fetch_and_uploadA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Load a file from a URL or local absolute path and store it as an Odoo ir.attachment. The MCP server handles the transfer — no binary passes through the AI context. Pass attachment_id to replace an existing attachment in-place (same ID, no arch update needed). Omit attachment_id to create a new attachment. is_image: process the file as an image (validated and optimised; Odoo rejects other files) — false for JS/CSS/HTML/JSON/fonts. Returns {id, src} usable in any context (arch_db, chatter, record field); src is /web/image/{id} for images, /web/content/{id} for other files. |
| download_binaryA | Download a binary field value from an Odoo record to a local absolute path on the MCP server filesystem. The binary is decoded and written to disk — no base64 passes through the AI context. Use this as the source step in a cross-instance binary migration: call download_binary on source MCP, then upload_binary on target MCP using the same path. Returns {success, dest_path, size_bytes} or {error}. |
| upload_binaryA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Upload a local file into an Odoo record's binary field. Reads the file at source_path (absolute path on the MCP server filesystem), encodes it, and writes it to the specified field via the ORM — no base64 in AI context. Use this as the target step in a cross-instance binary migration: call download_binary on source MCP first, then upload_binary on target MCP using the same path. Returns {success, model, record_id, field, size_bytes} or {error}. |
| translation_getA | Read all language translations for a translatable field on a record. Works on any field with translate=True (char fields: returns one entry per language) or callable translate (html / arch_db: returns one entry per translatable term per language). record_id and field_name each accept a single value OR an array (batch read in one call). langs: optional list of language codes to filter (e.g. ["fr_FR", "ar_001"]); omit to return all installed languages. Single record_id AND single field_name → {translations: [{lang, source, value}], translation_type, translation_show_source}. Any array argument → {results: [{record_id, field_name, translations, translation_type, translation_show_source} | {record_id, field_name, error}]}. Returns the above or {error}. |
| translation_updateA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Write translations for translatable field(s). Two forms: (1) Single/same-map — pass record_id (a number or an array of ids), field_name and translations; the same translations map is applied to every id. (2) Batch — pass updates: [{record_id, field_name, translations}, ...] to write different content per record and per field in one call (e.g. name + html_content for many records). For char fields (translate=True): translations = {"fr_FR": "Bonjour", "ar_001": "مرحبا"}. For html / arch_db fields (callable translate): translations = {"fr_FR": {"English source term": "French translation"}}. The target language must be installed in Odoo (Settings → Languages). Single id (number) form returns {success: true}; id-array and batch forms return {results: [{record_id, field_name, success: true} | {record_id, field_name, error}]}. Returns the above or {error}. |
| translation_auditA | Audit translation coverage and integrity for translatable field(s) across one or more records. For each record×field it reports total source terms and, per target language, how many are translated and which source terms are still missing. It also returns two integrity flags: suspect_source (when base_lang is English, source terms written in Arabic script — the signature of the "translation stored as source" defect that destroys the English body) and nonempty_base (terms whose base-language value is non-empty). Use it to verify a bilingual push in one call and to catch source corruption early. record_id and field_name each accept a single value or an array. base_lang defaults to "en_US"; target_langs defaults to every non-base language present. Long term lists are capped at max_list (default 50) with a *_truncated flag. Returns {passed, summary, results: [...]} or {error}. |
| createA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Create a new record. values: dict of field/value pairs (form-view fields only). Defaults are merged with provided values automatically. Pass action_id to include the action's context (e.g. default_partner_id). Pass context dict directly for wizard models. Returns {id, display_name} or {error}. |
| updateA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Update fields on an existing record. values: {field: value, ...}. Writes both form-view fields and model fields not exposed in the form view. One2many / many2many fields accept Odoo Command tuples directly: [[0,0,{vals}]] create+link, [[1,id,{vals}]] update line, [[2,id]] delete line, [[6,0,[ids]]] replace set. Pass context to control write behaviour — e.g. {lang: "fr_FR"} writes the value for that language on translate=True fields (without it, the user's language, as in the form), {mail_notrack: true} suppresses chatter entries. Returns {success, updated_fields, non_form_fields} or {error}. |
| execute_actionA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Execute a button or server action on a record. action: button name (method) or label as shown in get_model_actions — e.g. "action_confirm", "Confirm", "Privacy Lookup". View buttons (type=object) call the method directly; server actions use ir.actions.server.run. A button hidden on the record in its current state is refused, as in the form. Returns the Odoo action result, {success: true}, or {error}. |
| archiveA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Archive (deactivate) a record by setting active=False. Only works on models that have an active field (most standard models do). Returns {success: true} or {error}. |
| post_messageA | Post a plain-text message or internal note on a record (requires mail.thread). HTML in body is shown as text, as when typed in the chatter. message_type: "comment" (sent to followers) or "note" (internal log note, not emailed). Returns {message_id} or {error}. |
| schedule_activityB | Schedule an activity on a record. activity_type: name of the activity type (e.g. "To-Do", "Email", "Phone Call"). deadline: ISO date string YYYY-MM-DD. summary: short title. note: longer description (optional). assigned_user_id: who to assign (default: current user). Returns {activity_id, activity_type, deadline} or {error}. |
| list_pagesB | List website pages. Returns id, name, url, is_published, view_id, website_id for each page. |
| create_pageA | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Create a website page as the website editor's "New → Page" does: a view from the default page template plus the page, with a URL made unique from the name. The page is created unpublished (publish with set_page_visibility). add_menu: also add it to the main menu. website_id: target website (default: the current one). Returns {page_id, view_id, url, menu_id?} — fill it with get_page_arch / set_page_arch. |
| get_page_archA | Return the raw arch_db XML of a website page's view. Pass page_id from list_pages. Returns {view_id, arch_db}. The AI is responsible for reading and editing this XML. |
| set_page_archC | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Write arch_db XML to an ir.ui.view. Use view_id from get_page_arch. The caller is fully responsible for valid, well-formed XML. |
| set_page_visibilityC | 💡 Before multi-step work, check find_skill / list_workflows for canonical recipes. Publish or unpublish a website page. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 36 tools
Most tools target clearly distinct resources and actions (CRUD records vs website pages vs translations vs attachments), and the descriptions explicitly separate list_records from search_records. However, several introspection/action helpers overlap—get_model_actions, get_available_actions, and get_model_interface all expose button/action/CRUD data—and fetch_and_upload versus upload_binary both handle local files, creating a few confusing boundaries.
Nearly all names use snake_case with consistent families like list_*, get_*, set_page_*, and translation_*. Minor deviations exist (translation_get/update/audit use noun_verb while most use verb_noun; single verbs create/update/archive), but the conventions remain readable and predictable.
36 tools is well above the 3–15 sweet spot and the 25+ 'too many' threshold, indicating an overloaded surface for one MCP server. Several tools are convenience wrappers or near-duplicates (get_model_interface vs get_model_actions/get_fields, fetch_and_upload vs upload_binary), so not every tool clearly earns its place.
The surface covers CRUD (create/update/archive), read/search/group, actions/buttons, website pages, translations, attachments/binary migration, messaging/activities, and skills/workflows. Gaps exist—no hard delete/unlink operation (archive only), no report rendering/download despite reporting metadata, and no page/menu deletion—but core workflows are well-covered.