From dedf7014bdc5633ff5356787abf7456e99ef51fe Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 20 Apr 2026 09:17:16 +0200 Subject: [PATCH 01/21] [ADD] ai_tool --- ai_tool/README.rst | 145 ++++++++ ai_tool/__init__.py | 2 + ai_tool/__manifest__.py | 22 ++ ai_tool/data/ai_tools.xml | 22 ++ ai_tool/models/__init__.py | 1 + ai_tool/models/ai_tool.py | 94 +++++ ai_tool/readme/CONTRIBUTORS.md | 2 + ai_tool/readme/DESCRIPTION.md | 3 + ai_tool/readme/USAGE.md | 52 +++ ai_tool/security/ir.model.access.csv | 2 + ai_tool/static/description/icon.png | Bin 0 -> 9455 bytes ai_tool/static/description/index.html | 489 ++++++++++++++++++++++++++ ai_tool/tests/__init__.py | 1 + ai_tool/tests/test_ai_tool.py | 49 +++ ai_tool/tools.py | 18 + ai_tool/views/ai_tool.xml | 55 +++ ai_tool/views/menu.xml | 6 + 17 files changed, 963 insertions(+) create mode 100644 ai_tool/README.rst create mode 100644 ai_tool/__init__.py create mode 100644 ai_tool/__manifest__.py create mode 100644 ai_tool/data/ai_tools.xml create mode 100644 ai_tool/models/__init__.py create mode 100644 ai_tool/models/ai_tool.py create mode 100644 ai_tool/readme/CONTRIBUTORS.md create mode 100644 ai_tool/readme/DESCRIPTION.md create mode 100644 ai_tool/readme/USAGE.md create mode 100644 ai_tool/security/ir.model.access.csv create mode 100644 ai_tool/static/description/icon.png create mode 100644 ai_tool/static/description/index.html create mode 100644 ai_tool/tests/__init__.py create mode 100644 ai_tool/tests/test_ai_tool.py create mode 100644 ai_tool/tools.py create mode 100644 ai_tool/views/ai_tool.xml create mode 100644 ai_tool/views/menu.xml diff --git a/ai_tool/README.rst b/ai_tool/README.rst new file mode 100644 index 00000000..5e06e332 --- /dev/null +++ b/ai_tool/README.rst @@ -0,0 +1,145 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======= +Ai Tool +======= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:24aba4610958ac085492da4b1fd0fe517144c8824ae25d8d3f6c44fe45c1388e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github + :target: https://github.com/OCA/ai/tree/16.0/ai_tool + :alt: OCA/ai +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This technical module provides the base infrastructure for defining AI +tools in Odoo. It allows other modules to register callable functions as +AI tools, which can then be used by MCP servers, automation flows, or +any AI-native integration. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module is technical, however, it adds some specific functions that +might be used in glue modules. + +For example, if we want to add on sales a functionality to find the +sales on a period, we should do: + +.. code:: xml + + + + total_sale_order + Calculate the total amount of sale orders within a date range and optionally for a specific customer. + + _mcp_total_sale_order + + + +.. code:: python + + from odoo.addons.ai_tool import aitool + + class SaleOrder(models.Model): + _inherit = "sale.order" + + @aitool( + input_schema={ + "start_date": {"type": "date"}, + "end_date": {"type": "date"}, + "customer_id": {"type": "integer"}, + }, + required_inputs=["start_date", "end_date"], + output_schema={ + "amount_total": {"type": "number"}, + }, + ) + def _mcp_total_sale_order(self, start_date, end_date, customer_id=None): + domain = [("date_order", ">=", start_date), ("date_order", "<=", end_date)] + if customer_id: + domain.append(("partner_id", "=", customer_id)) + orders = self.read_group(domain, ["amount_total"], []) + return { + "amount_total": (orders[0]["amount_total"] or 0) if orders else 0, + } + +Be aware that this kind of elements must allways return a dict. All the +elements will be defined in output_schema. + +Also, for the signature of the functions, all fields must be in the +inputs with the exception of record. This argument is protected and is +used to define integrations with automation. This argument is required +in ``generic_model`` and ``record`` tools. + +On ``generic_model``\ s we are expecting this value because we want to +do a specific action with the model. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/ai `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_tool/__init__.py b/ai_tool/__init__.py new file mode 100644 index 00000000..738a2eec --- /dev/null +++ b/ai_tool/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import tools diff --git a/ai_tool/__manifest__.py b/ai_tool/__manifest__.py new file mode 100644 index 00000000..e69e5037 --- /dev/null +++ b/ai_tool/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Ai Tool", + "summary": """We want to generate some specific AI Tools + that might be used in other places, like MCP or native.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/ai", + "depends": [ + "mail", + ], + "data": [ + "security/ir.model.access.csv", + "views/menu.xml", + "views/ai_tool.xml", + "data/ai_tools.xml", + ], + "demo": [], +} diff --git a/ai_tool/data/ai_tools.xml b/ai_tool/data/ai_tools.xml new file mode 100644 index 00000000..d21afe69 --- /dev/null +++ b/ai_tool/data/ai_tools.xml @@ -0,0 +1,22 @@ + + + + + + get_date + Get the current date. + + _ai_get_date + generic + + + + post_message + Post a message to a record. + + _ai_post_message + generic_model + + + diff --git a/ai_tool/models/__init__.py b/ai_tool/models/__init__.py new file mode 100644 index 00000000..b6c10aa8 --- /dev/null +++ b/ai_tool/models/__init__.py @@ -0,0 +1 @@ +from . import ai_tool diff --git a/ai_tool/models/ai_tool.py b/ai_tool/models/ai_tool.py new file mode 100644 index 00000000..c01eee29 --- /dev/null +++ b/ai_tool/models/ai_tool.py @@ -0,0 +1,94 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import date + +from odoo import fields, models +from odoo.tools.mail import html_sanitize, plaintext2html + +from ..tools import aitool + +try: + import markdown +except ImportError: + markdown = None + + +class AiTool(models.Model): + + _name = "ai.tool" + _description = "AI Tool" + + name = fields.Char(required=True) + description = fields.Text() + model_id = fields.Many2one( + "ir.model", readonly=True, required=True, ondelete="cascade" + ) + function_name = fields.Char(readonly=True, required=True) + kind = fields.Selection( + [ + ("generic", "Generic"), + ("generic_model", "Generic but requires a record to work"), + ("record", "Record"), + ], + readonly=True, + required=True, + default="record", + ) + + def _get_tool_definition(self): + func = getattr(self.env[self.model_id.model], self.function_name) + return { + "name": self.name, + "description": self.description, + "inputSchema": func._ai_tool["input_schema"], + "outputSchema": func._ai_tool["output_schema"], + } + + @aitool( + input_schema={}, + output_schema={ + "date": {"type": "date"}, + }, + ) + def _ai_get_date(self): + return {"date": date.today().isoformat()} + + @aitool( + input_schema={ + "message": {"type": "string"}, + }, + required_inputs=["message"], + output_schema={}, + ) + def _ai_post_message(self, message=None, record=None, **kwargs): + if not record or not record.exists(): + raise ValueError("Record must be provided and exist to post a message") + record.message_post(body=self._ai_post_message_parse_body(message)) + return {} + + def _ai_post_message_parse_body(self, message): + """ + Using markdown library if available to convert markdown to html, + otherwise using plaintext2html as fallback + """ + if markdown: + return html_sanitize(markdown.markdown(message)) + return plaintext2html(message) + + def _execute_tool(self, *args, record=None, **kwargs): + if self.kind == "generic": + return getattr(self.env[self.model_id.model], self.function_name)( + *args, **kwargs + ) + if not record: + raise ValueError("Record must be provided for non-generic tools") + if self.kind == "generic_model": + return getattr(self.env[self.model_id.model], self.function_name)( + *args, record=record, **kwargs + ) + elif record._name != self.model_id.model: + raise ValueError( + f"Record model {record._name} does not match tool model {self.model_id.model}" + ) + return getattr(record, self.function_name)(*args, **kwargs) or {} diff --git a/ai_tool/readme/CONTRIBUTORS.md b/ai_tool/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..2c066ba7 --- /dev/null +++ b/ai_tool/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/ai_tool/readme/DESCRIPTION.md b/ai_tool/readme/DESCRIPTION.md new file mode 100644 index 00000000..71747f87 --- /dev/null +++ b/ai_tool/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This technical module provides the base infrastructure for defining AI tools in Odoo. +It allows other modules to register callable functions as AI tools, which can then be +used by MCP servers, automation flows, or any AI-native integration. diff --git a/ai_tool/readme/USAGE.md b/ai_tool/readme/USAGE.md new file mode 100644 index 00000000..c18e4ae5 --- /dev/null +++ b/ai_tool/readme/USAGE.md @@ -0,0 +1,52 @@ +This module is technical, however, it adds some specific functions that might be used in glue modules. + +For example, if we want to add on sales a functionality to find the sales on a period, we should do: + +```xml + + + total_sale_order + Calculate the total amount of sale orders within a date range and optionally for a specific customer. + + _mcp_total_sale_order + + +``` + +```python +from odoo.addons.ai_tool import aitool + +class SaleOrder(models.Model): + _inherit = "sale.order" + + @aitool( + input_schema={ + "start_date": {"type": "date"}, + "end_date": {"type": "date"}, + "customer_id": {"type": "integer"}, + }, + required_inputs=["start_date", "end_date"], + output_schema={ + "amount_total": {"type": "number"}, + }, + ) + def _mcp_total_sale_order(self, start_date, end_date, customer_id=None): + domain = [("date_order", ">=", start_date), ("date_order", "<=", end_date)] + if customer_id: + domain.append(("partner_id", "=", customer_id)) + orders = self.read_group(domain, ["amount_total"], []) + return { + "amount_total": (orders[0]["amount_total"] or 0) if orders else 0, + } + +``` + +Be aware that this kind of elements must allways return a dict. All the elements will be defined in output_schema. + +Also, for the signature of the functions, all fields must be in the inputs with the exception of record. +This argument is protected and is used to define integrations with automation. +This argument is required in `generic_model` and `record` tools. + +On `generic_model`s we are expecting this value because we want to do a specific action with the model. diff --git a/ai_tool/security/ir.model.access.csv b/ai_tool/security/ir.model.access.csv new file mode 100644 index 00000000..4f6beed7 --- /dev/null +++ b/ai_tool/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ai_tool,access_ai_tool,model_ai_tool,base.group_user,1,0,0,0 diff --git a/ai_tool/static/description/icon.png b/ai_tool/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html new file mode 100644 index 00000000..def5e728 --- /dev/null +++ b/ai_tool/static/description/index.html @@ -0,0 +1,489 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Ai Tool

+ +

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

This technical module provides the base infrastructure for defining AI +tools in Odoo. It allows other modules to register callable functions as +AI tools, which can then be used by MCP servers, automation flows, or +any AI-native integration.

+

Table of contents

+ +
+

Usage

+

This module is technical, however, it adds some specific functions that +might be used in glue modules.

+

For example, if we want to add on sales a functionality to find the +sales on a period, we should do:

+
+<odoo>
+    <record model="ai.tool" id="total_sale_order_tool">
+        <field name="name">total_sale_order</field>
+        <field
+            name="description"
+        >Calculate the total amount of sale orders within a date range and optionally for a specific customer.</field>
+        <field name="model_id" ref="model_sale_order" />
+        <field name="function_name">_mcp_total_sale_order</field>
+    </record>
+</odoo>
+
+
+from odoo.addons.ai_tool import aitool
+
+class SaleOrder(models.Model):
+    _inherit = "sale.order"
+
+    @aitool(
+        input_schema={
+            "start_date": {"type": "date"},
+            "end_date": {"type": "date"},
+            "customer_id": {"type": "integer"},
+        },
+        required_inputs=["start_date", "end_date"],
+        output_schema={
+            "amount_total": {"type": "number"},
+        },
+    )
+    def _mcp_total_sale_order(self, start_date, end_date, customer_id=None):
+        domain = [("date_order", ">=", start_date), ("date_order", "<=", end_date)]
+        if customer_id:
+            domain.append(("partner_id", "=", customer_id))
+        orders = self.read_group(domain, ["amount_total"], [])
+        return {
+            "amount_total": (orders[0]["amount_total"] or 0) if orders else 0,
+        }
+
+

Be aware that this kind of elements must allways return a dict. All the +elements will be defined in output_schema.

+

Also, for the signature of the functions, all fields must be in the +inputs with the exception of record. This argument is protected and is +used to define integrations with automation. This argument is required +in generic_model and record tools.

+

On generic_models we are expecting this value because we want to +do a specific action with the model.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/ai project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/ai_tool/tests/__init__.py b/ai_tool/tests/__init__.py new file mode 100644 index 00000000..30e42873 --- /dev/null +++ b/ai_tool/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ai_tool diff --git a/ai_tool/tests/test_ai_tool.py b/ai_tool/tests/test_ai_tool.py new file mode 100644 index 00000000..c9090aaf --- /dev/null +++ b/ai_tool/tests/test_ai_tool.py @@ -0,0 +1,49 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from odoo.tests.common import TransactionCase + + +class TestAiTool(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + + def test_tool(self): + definition = self.env.ref("ai_tool.current_date")._get_tool_definition() + self.assertEqual(definition["inputSchema"]["type"], "object") + self.assertEqual(definition["outputSchema"]["type"], "object") + + def test_get_date_tool(self): + with freeze_time("2024-01-01"): + tool = self.env.ref("ai_tool.current_date") + result = tool._execute_tool(record=tool) + self.assertEqual(result["date"], "2024-01-01") + + def test_post_message_tool(self): + partner = self.partner + messages = partner.message_ids + tool = self.env.ref("ai_tool.post_message") + tool._execute_tool(message="Hello World", record=partner) + self.assertEqual(len(partner.message_ids), len(messages) + 1) + self.assertRegex((partner.message_ids - messages).body, "Hello World") + + def test_tool_no_record(self): + tool = self.env.ref("ai_tool.post_message") + with self.assertRaises(ValueError): + tool._execute_tool(message="Hello World") + + def test_post_message_tool_no_record(self): + tool = self.env.ref("ai_tool.post_message") + tool.kind = "generic" + with self.assertRaises(ValueError): + tool._execute_tool(message="Hello World", record=self.partner) + + def test_post_message_tool_record_different_model(self): + tool = self.env.ref("ai_tool.post_message") + tool.kind = "record" + with self.assertRaises(ValueError): + tool._execute_tool(message="Hello World", record=self.partner) diff --git a/ai_tool/tools.py b/ai_tool/tools.py new file mode 100644 index 00000000..443a1b56 --- /dev/null +++ b/ai_tool/tools.py @@ -0,0 +1,18 @@ +from odoo.api import attrsetter + + +def aitool(input_schema: dict, output_schema: dict, required_inputs: list = None): + return attrsetter( + "_ai_tool", + { + "input_schema": { + "type": "object", + "properties": input_schema, + "required": required_inputs or [], + }, + "output_schema": { + "type": "object", + "properties": output_schema, + }, + }, + ) diff --git a/ai_tool/views/ai_tool.xml b/ai_tool/views/ai_tool.xml new file mode 100644 index 00000000..7ce1a38b --- /dev/null +++ b/ai_tool/views/ai_tool.xml @@ -0,0 +1,55 @@ + + + + + + ai.tool + +
+
+ + + + + + + + + + + + + ai.tool + + + + + + + + + ai.tool + + + + + + + + + AI Tool + ai.tool + tree,form + [] + {} + + + + AI Tool + + + + + + diff --git a/ai_tool/views/menu.xml b/ai_tool/views/menu.xml new file mode 100644 index 00000000..f897888c --- /dev/null +++ b/ai_tool/views/menu.xml @@ -0,0 +1,6 @@ + + + + + From 98d623ac3c031b43c32d6d36b2053b4f3cfcb386 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 14 May 2026 08:46:27 +0000 Subject: [PATCH 02/21] [UPD] Update ai_tool.pot --- ai_tool/i18n/ai_tool.pot | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 ai_tool/i18n/ai_tool.pot diff --git a/ai_tool/i18n/ai_tool.pot b/ai_tool/i18n/ai_tool.pot new file mode 100644 index 00000000..b2ece4bf --- /dev/null +++ b/ai_tool/i18n/ai_tool.pot @@ -0,0 +1,101 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_tool +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_menu_root +msgid "AI" +msgstr "" + +#. module: ai_tool +#: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window +#: model:ir.model,name:ai_tool.model_ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_tool_menu +msgid "AI Tool" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +msgid "Created on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description +msgid "Description" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name +msgid "Function Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +msgid "Generic" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model +msgid "Generic but requires a record to work" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id +msgid "ID" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind +msgid "Kind" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update +msgid "Last Modified on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +msgid "Model" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +msgid "Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +msgid "Record" +msgstr "" From e334152144cc33f470972dfd0ffc18f014ff9929 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 14 May 2026 08:49:14 +0000 Subject: [PATCH 03/21] [BOT] post-merge updates --- ai_tool/README.rst | 2 +- ai_tool/static/description/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ai_tool/README.rst b/ai_tool/README.rst index 5e06e332..1652182b 100644 --- a/ai_tool/README.rst +++ b/ai_tool/README.rst @@ -11,7 +11,7 @@ Ai Tool !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:24aba4610958ac085492da4b1fd0fe517144c8824ae25d8d3f6c44fe45c1388e + !! source digest: sha256:b0dec31c213cecc194ff875e340eb11844acb3ee0e5147864575a90b02f288bf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html index def5e728..5201087a 100644 --- a/ai_tool/static/description/index.html +++ b/ai_tool/static/description/index.html @@ -372,7 +372,7 @@

Ai Tool

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:24aba4610958ac085492da4b1fd0fe517144c8824ae25d8d3f6c44fe45c1388e +!! source digest: sha256:b0dec31c213cecc194ff875e340eb11844acb3ee0e5147864575a90b02f288bf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

This technical module provides the base infrastructure for defining AI From 3fb2c1b7a769e33a4f9d1dec0dad9641295559c2 Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 14 May 2026 13:15:55 +0000 Subject: [PATCH 04/21] Added translation using Weblate (Italian) --- ai_tool/i18n/it.po | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 ai_tool/i18n/it.po diff --git a/ai_tool/i18n/it.po b/ai_tool/i18n/it.po new file mode 100644 index 00000000..ffa9906d --- /dev/null +++ b/ai_tool/i18n/it.po @@ -0,0 +1,102 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_tool +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_menu_root +msgid "AI" +msgstr "" + +#. module: ai_tool +#: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window +#: model:ir.model,name:ai_tool.model_ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_tool_menu +msgid "AI Tool" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +msgid "Created on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description +msgid "Description" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name +msgid "Function Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +msgid "Generic" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model +msgid "Generic but requires a record to work" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id +msgid "ID" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind +msgid "Kind" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update +msgid "Last Modified on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +msgid "Model" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +msgid "Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +msgid "Record" +msgstr "" From d842e7ea6040269e34379da28c927612c97482ec Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 18 May 2026 09:55:29 +0200 Subject: [PATCH 05/21] [IMP] ai_tool: Pre-commit stuff --- ai_tool/data/ai_tools.xml | 2 -- ai_tool/models/ai_tool.py | 4 ++-- ai_tool/pyproject.toml | 3 +++ ai_tool/views/ai_tool.xml | 2 -- 4 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 ai_tool/pyproject.toml diff --git a/ai_tool/data/ai_tools.xml b/ai_tool/data/ai_tools.xml index d21afe69..e6f7ce5a 100644 --- a/ai_tool/data/ai_tools.xml +++ b/ai_tool/data/ai_tools.xml @@ -2,7 +2,6 @@ - get_date Get the current date. @@ -18,5 +17,4 @@ _ai_post_message generic_model - diff --git a/ai_tool/models/ai_tool.py b/ai_tool/models/ai_tool.py index c01eee29..68cd76d3 100644 --- a/ai_tool/models/ai_tool.py +++ b/ai_tool/models/ai_tool.py @@ -15,7 +15,6 @@ class AiTool(models.Model): - _name = "ai.tool" _description = "AI Tool" @@ -89,6 +88,7 @@ def _execute_tool(self, *args, record=None, **kwargs): ) elif record._name != self.model_id.model: raise ValueError( - f"Record model {record._name} does not match tool model {self.model_id.model}" + f"Record model {record._name} does not match tool " + f"model {self.model_id.model}" ) return getattr(record, self.function_name)(*args, **kwargs) or {} diff --git a/ai_tool/pyproject.toml b/ai_tool/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/ai_tool/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/ai_tool/views/ai_tool.xml b/ai_tool/views/ai_tool.xml index 7ce1a38b..f6f425f5 100644 --- a/ai_tool/views/ai_tool.xml +++ b/ai_tool/views/ai_tool.xml @@ -2,7 +2,6 @@ - ai.tool @@ -51,5 +50,4 @@ - From 0d2c1c0b21f7f32f134f6e1bca5fec5919624879 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 18 May 2026 09:58:15 +0200 Subject: [PATCH 06/21] [MIG] ai_tool: Migration to 18.0 --- ai_tool/README.rst | 18 ++++++--------- ai_tool/__manifest__.py | 2 +- ai_tool/readme/USAGE.md | 2 +- ai_tool/static/description/index.html | 32 +++++++++++---------------- ai_tool/views/ai_tool.xml | 8 +++---- 5 files changed, 26 insertions(+), 36 deletions(-) diff --git a/ai_tool/README.rst b/ai_tool/README.rst index 1652182b..54b5eb25 100644 --- a/ai_tool/README.rst +++ b/ai_tool/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======= Ai Tool ======= @@ -17,17 +13,17 @@ Ai Tool .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github - :target: https://github.com/OCA/ai/tree/16.0/ai_tool + :target: https://github.com/OCA/ai/tree/18.0/ai_tool :alt: OCA/ai .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool + :target: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_tool :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -66,7 +62,7 @@ sales on a period, we should do: .. code:: python - from odoo.addons.ai_tool import aitool + from odoo.addons.ai_tool.tools import aitool class SaleOrder(models.Model): _inherit = "sale.order" @@ -108,7 +104,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -140,6 +136,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/ai `_ project on GitHub. +This module is part of the `OCA/ai `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_tool/__manifest__.py b/ai_tool/__manifest__.py index e69e5037..462041d5 100644 --- a/ai_tool/__manifest__.py +++ b/ai_tool/__manifest__.py @@ -5,7 +5,7 @@ "name": "Ai Tool", "summary": """We want to generate some specific AI Tools that might be used in other places, like MCP or native.""", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_tool/readme/USAGE.md b/ai_tool/readme/USAGE.md index c18e4ae5..c7f118c5 100644 --- a/ai_tool/readme/USAGE.md +++ b/ai_tool/readme/USAGE.md @@ -16,7 +16,7 @@ For example, if we want to add on sales a functionality to find the sales on a p ``` ```python -from odoo.addons.ai_tool import aitool +from odoo.addons.ai_tool.tools import aitool class SaleOrder(models.Model): _inherit = "sale.order" diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html index 5201087a..b3cff61d 100644 --- a/ai_tool/static/description/index.html +++ b/ai_tool/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Ai Tool -

+
+

Ai Tool

- - -Odoo Community Association - -
-

Ai Tool

-

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

This technical module provides the base infrastructure for defining AI tools in Odoo. It allows other modules to register callable functions as AI tools, which can then be used by MCP servers, automation flows, or @@ -393,7 +388,7 @@

Ai Tool

-

Usage

+

Usage

This module is technical, however, it adds some specific functions that might be used in glue modules.

For example, if we want to add on sales a functionality to find the @@ -411,7 +406,7 @@

Usage

</odoo>
-from odoo.addons.ai_tool import aitool
+from odoo.addons.ai_tool.tools import aitool
 
 class SaleOrder(models.Model):
     _inherit = "sale.order"
@@ -446,23 +441,23 @@ 

Usage

do a specific action with the model.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -479,11 +474,10 @@

Maintainers

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/ai project on GitHub.

+

This module is part of the OCA/ai project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

-
diff --git a/ai_tool/views/ai_tool.xml b/ai_tool/views/ai_tool.xml index f6f425f5..56d3cd87 100644 --- a/ai_tool/views/ai_tool.xml +++ b/ai_tool/views/ai_tool.xml @@ -27,19 +27,19 @@ - + ai.tool - + - + AI Tool ai.tool - tree,form + list,form [] {} From 9594e185df053d0311c0f5edfce52bde6d336045 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 19 Jun 2026 10:20:25 +0000 Subject: [PATCH 07/21] [UPD] Update ai_tool.pot --- ai_tool/i18n/ai_tool.pot | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ai_tool/i18n/ai_tool.pot b/ai_tool/i18n/ai_tool.pot index b2ece4bf..bd7a82ea 100644 --- a/ai_tool/i18n/ai_tool.pot +++ b/ai_tool/i18n/ai_tool.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -70,11 +70,6 @@ msgstr "" msgid "Kind" msgstr "" -#. module: ai_tool -#: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update -msgid "Last Modified on" -msgstr "" - #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid msgid "Last Updated by" From e69f79d14b54b1de929a14f885330804743ee0e4 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 19 Jun 2026 10:23:16 +0000 Subject: [PATCH 08/21] [BOT] post-merge updates --- ai_tool/README.rst | 8 ++++++-- ai_tool/static/description/index.html | 28 ++++++++++++++++----------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/ai_tool/README.rst b/ai_tool/README.rst index 54b5eb25..4ee33956 100644 --- a/ai_tool/README.rst +++ b/ai_tool/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ======= Ai Tool ======= @@ -7,13 +11,13 @@ Ai Tool !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b0dec31c213cecc194ff875e340eb11844acb3ee0e5147864575a90b02f288bf + !! source digest: sha256:84d38912b0ba3a8c32a3cb1957183d7cb15fd310cc91c81e76f4d5eca7b731e7 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html index b3cff61d..4ec0006c 100644 --- a/ai_tool/static/description/index.html +++ b/ai_tool/static/description/index.html @@ -3,7 +3,7 @@ -Ai Tool +README.rst -
-

Ai Tool

+
+ + +Odoo Community Association + +
+

Ai Tool

-

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

This technical module provides the base infrastructure for defining AI tools in Odoo. It allows other modules to register callable functions as AI tools, which can then be used by MCP servers, automation flows, or @@ -388,7 +393,7 @@

Ai Tool

-

Usage

+

Usage

This module is technical, however, it adds some specific functions that might be used in glue modules.

For example, if we want to add on sales a functionality to find the @@ -441,7 +446,7 @@

Usage

do a specific action with the model.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -449,15 +454,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -479,5 +484,6 @@

Maintainers

+
From 7a6e29bbac5336e838d78c0c8a5ec5eb60a6db6d Mon Sep 17 00:00:00 2001 From: Ed-Spain Date: Sun, 28 Jun 2026 12:21:37 +0000 Subject: [PATCH 09/21] Added translation using Weblate (Spanish) --- ai_tool/i18n/es.po | 97 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 ai_tool/i18n/es.po diff --git a/ai_tool/i18n/es.po b/ai_tool/i18n/es.po new file mode 100644 index 00000000..3cc9affe --- /dev/null +++ b/ai_tool/i18n/es.po @@ -0,0 +1,97 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_tool +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_menu_root +msgid "AI" +msgstr "" + +#. module: ai_tool +#: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window +#: model:ir.model,name:ai_tool.model_ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_tool_menu +msgid "AI Tool" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +msgid "Created on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description +msgid "Description" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name +msgid "Function Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +msgid "Generic" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model +msgid "Generic but requires a record to work" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id +msgid "ID" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind +msgid "Kind" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +msgid "Model" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +msgid "Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +msgid "Record" +msgstr "" From 6bbae95a0977d899fcd4087432a4768861e58e6b Mon Sep 17 00:00:00 2001 From: Ed-Spain Date: Sun, 28 Jun 2026 12:24:16 +0000 Subject: [PATCH 10/21] Translated using Weblate (Spanish) Currently translated at 50.0% (8 of 16 strings) Translation: ai-18.0/ai-18.0-ai_tool Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_tool/es/ --- ai_tool/i18n/es.po | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/ai_tool/i18n/es.po b/ai_tool/i18n/es.po index 3cc9affe..5de5e901 100644 --- a/ai_tool/i18n/es.po +++ b/ai_tool/i18n/es.po @@ -6,92 +6,102 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-06-28 12:24+0000\n" +"Last-Translator: Ed-Spain \n" "Language-Team: none\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" #. module: ai_tool #: model:ir.ui.menu,name:ai_tool.ai_menu_root msgid "AI" -msgstr "" +msgstr "IA" #. module: ai_tool #: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window #: model:ir.model,name:ai_tool.model_ai_tool #: model:ir.ui.menu,name:ai_tool.ai_tool_menu msgid "AI Tool" -msgstr "" +msgstr "Herramienta IA" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid msgid "Created by" -msgstr "" +msgstr "Creado por" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +#, fuzzy msgid "Created on" -msgstr "" +msgstr "Creada el" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description msgid "Description" -msgstr "" +msgstr "Descripción" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +#, fuzzy msgid "Display Name" -msgstr "" +msgstr "Nombre a Mostrar" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name msgid "Function Name" -msgstr "" +msgstr "Nombre de la Función" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +#, fuzzy msgid "Generic" -msgstr "" +msgstr "Genérico" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model msgid "Generic but requires a record to work" -msgstr "" +msgstr "Genérico pero requiere un registro para funcionar" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id msgid "ID" -msgstr "" +msgstr "ID" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind msgid "Kind" -msgstr "" +msgstr "Tipo" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +#, fuzzy msgid "Last Updated by" -msgstr "" +msgstr "Modificado por última vez por" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +#, fuzzy msgid "Last Updated on" -msgstr "" +msgstr "Actualizado por Última vez el" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +#, fuzzy msgid "Model" -msgstr "" +msgstr "Modelo" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +#, fuzzy msgid "Name" -msgstr "" +msgstr "Nombre" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +#, fuzzy msgid "Record" -msgstr "" +msgstr "Registro" From 6fb9c93c8a6d14d28efcb122d48803ce266f04f6 Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 1 Jul 2026 09:32:50 +0000 Subject: [PATCH 11/21] Translated using Weblate (Italian) Currently translated at 100.0% (17 of 17 strings) Translation: ai-18.0/ai-18.0-ai_tool Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_tool/it/ --- ai_tool/i18n/it.po | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/ai_tool/i18n/it.po b/ai_tool/i18n/it.po index ffa9906d..20aa0c1b 100644 --- a/ai_tool/i18n/it.po +++ b/ai_tool/i18n/it.po @@ -6,97 +6,99 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-07-01 11:46+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" #. module: ai_tool #: model:ir.ui.menu,name:ai_tool.ai_menu_root msgid "AI" -msgstr "" +msgstr "IA" #. module: ai_tool #: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window #: model:ir.model,name:ai_tool.model_ai_tool #: model:ir.ui.menu,name:ai_tool.ai_tool_menu msgid "AI Tool" -msgstr "" +msgstr "Strumento IA" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description msgid "Description" -msgstr "" +msgstr "Descrizione" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name msgid "Function Name" -msgstr "" +msgstr "Nome funzione" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic msgid "Generic" -msgstr "" +msgstr "Generico" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model msgid "Generic but requires a record to work" -msgstr "" +msgstr "Generico ma richiede un record per lavorare" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id msgid "ID" -msgstr "" +msgstr "ID" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind msgid "Kind" -msgstr "" +msgstr "Genere" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento di" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id msgid "Model" -msgstr "" +msgstr "Modello" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name msgid "Name" -msgstr "" +msgstr "Nome" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record msgid "Record" -msgstr "" +msgstr "Record" From 2a2ed147e049af86ed27d40b4987ded3299c7494 Mon Sep 17 00:00:00 2001 From: Ed-Spain Date: Fri, 24 Jul 2026 17:27:26 +0000 Subject: [PATCH 12/21] Translated using Weblate (Spanish) Currently translated at 81.2% (13 of 16 strings) Translation: ai-18.0/ai-18.0-ai_tool Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_tool/es/ --- ai_tool/i18n/es.po | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ai_tool/i18n/es.po b/ai_tool/i18n/es.po index 5de5e901..eb6796ef 100644 --- a/ai_tool/i18n/es.po +++ b/ai_tool/i18n/es.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-06-28 12:24+0000\n" +"PO-Revision-Date: 2026-07-24 17:27+0000\n" "Last-Translator: Ed-Spain \n" "Language-Team: none\n" "Language: es\n" @@ -35,7 +35,6 @@ msgstr "Creado por" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date -#, fuzzy msgid "Created on" msgstr "Creada el" @@ -46,7 +45,6 @@ msgstr "Descripción" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name -#, fuzzy msgid "Display Name" msgstr "Nombre a Mostrar" @@ -57,7 +55,6 @@ msgstr "Nombre de la Función" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic -#, fuzzy msgid "Generic" msgstr "Genérico" @@ -84,7 +81,6 @@ msgstr "Modificado por última vez por" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date -#, fuzzy msgid "Last Updated on" msgstr "Actualizado por Última vez el" @@ -96,7 +92,6 @@ msgstr "Modelo" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name -#, fuzzy msgid "Name" msgstr "Nombre" From 1bde48018e212c615678cb8799467f89b28e60d7 Mon Sep 17 00:00:00 2001 From: Matteo Boscolo Date: Thu, 13 Aug 2026 08:44:51 +0200 Subject: [PATCH 13/21] [MIG] ai_tool: Migration to 19.0 --- ai_tool/README.rst | 14 +++++++++----- ai_tool/__manifest__.py | 2 +- ai_tool/readme/CONTRIBUTORS.md | 2 ++ ai_tool/static/description/index.html | 12 ++++++++---- ai_tool/tools.py | 2 +- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ai_tool/README.rst b/ai_tool/README.rst index 4ee33956..34fd138c 100644 --- a/ai_tool/README.rst +++ b/ai_tool/README.rst @@ -21,13 +21,13 @@ Ai Tool :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github - :target: https://github.com/OCA/ai/tree/18.0/ai_tool + :target: https://github.com/OCA/ai/tree/19.0/ai_tool :alt: OCA/ai .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_tool + :target: https://translation.odoo-community.org/projects/ai-19-0/ai-19-0-ai_tool :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -108,7 +108,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -127,6 +127,10 @@ Contributors - Enric Tobella +- `OmniaSolutions `__ + + - Matteo Boscolo + Maintainers ----------- @@ -140,6 +144,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/ai `_ project on GitHub. +This module is part of the `OCA/ai `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_tool/__manifest__.py b/ai_tool/__manifest__.py index 462041d5..523cc55f 100644 --- a/ai_tool/__manifest__.py +++ b/ai_tool/__manifest__.py @@ -5,7 +5,7 @@ "name": "Ai Tool", "summary": """We want to generate some specific AI Tools that might be used in other places, like MCP or native.""", - "version": "18.0.1.0.0", + "version": "19.0.1.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_tool/readme/CONTRIBUTORS.md b/ai_tool/readme/CONTRIBUTORS.md index 2c066ba7..207c7974 100644 --- a/ai_tool/readme/CONTRIBUTORS.md +++ b/ai_tool/readme/CONTRIBUTORS.md @@ -1,2 +1,4 @@ - [Dixmit](https://www.dixmit.com) - Enric Tobella +- [OmniaSolutions](https://www.omniasolutions.website) + - Matteo Boscolo \ diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html index 4ec0006c..8fcf23a0 100644 --- a/ai_tool/static/description/index.html +++ b/ai_tool/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Ai Tool + + +
+ + + +Odoo Community Association + +
+

Ai Oca Mcp

+ +

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

This module exposes Odoo AI tools as an MCP (Model Context Protocol) +server, allowing external AI clients such as n8n or custom agents to +call Odoo functions via the standardized MCP protocol. Authentication is +handled via per-client API keys.

+

Note: Claude Desktop requires OAuth 2.0, which is not supported directly +by this module. An extension could be required.

+

Table of contents

+ +
+

Usage

+
    +
  • Access in Developer mode
  • +
  • Go to AI > MCP Server
  • +
  • Create a new MCP Server and add the generic tools you want to +expose
  • +
  • Click Add Key to generate a new API key for a client — the key is +only shown once
  • +
  • Use the provided URL and the generated key to configure your AI client
  • +
+
+

Connecting from n8n

+

Use the MCP node with:

+
    +
  • URL: the value shown in the URL field
  • +
  • Authentication: Bearer Token
  • +
  • Token: the generated API key
  • +
+
+
+

Tool limitations

+

Only tools of kind generic are supported. Tools requiring a record +context (generic_model, record) cannot be used via MCP.

+
+
+

Security

+

Each client should have its own API key. Keys can be expired +individually from the server form or from AI > MCP Server Log to +audit all calls.

+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/ai project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/ai_oca_mcp/tests/__init__.py b/ai_oca_mcp/tests/__init__.py new file mode 100644 index 00000000..cb03ee12 --- /dev/null +++ b/ai_oca_mcp/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mcp diff --git a/ai_oca_mcp/tests/test_mcp.py b/ai_oca_mcp/tests/test_mcp.py new file mode 100644 index 00000000..15e3e454 --- /dev/null +++ b/ai_oca_mcp/tests/test_mcp.py @@ -0,0 +1,219 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from freezegun import freeze_time + +from odoo.tests.common import HttpCase +from odoo.tools import mute_logger + + +class TestMcp(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.server = cls.env["mcp.server"].create( + { + "name": "Test Server", + "tool_ids": [(4, cls.env.ref("ai_tool.current_date").id)], + } + ) + wizard = ( + cls.env["mcp.server.key.add"] + .with_context(default_server_id=cls.server.id) + .create({"name": "Test Key"}) + ) + wizard.generate_key() + cls.security_key = wizard.key + + @mute_logger("odoo.http") + def test_no_authorization(self): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + } + ), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(request.status_code, 401) + + def test_wrong_authorization(self): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": "Bearer wrong", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("error", response) + + def test_wrong_method(self): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "wrong_method", + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("error", response) + + def test_correct_initialize(self): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("result", response) + self.assertIn("capabilities", response["result"]) + self.assertIn("tools", response["result"]["capabilities"]) + + def test_list_tools(self): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "tools/list", + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("result", response) + self.assertIn("tools", response["result"]) + self.assertEqual(1, len(response["result"]["tools"])) + self.assertEqual("get_date", response["result"]["tools"][0]["name"]) + + def test_execute_wrong_tool(self): + with freeze_time("2024-01-01"): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "tools/call", + "params": { + "name": "post_message", + "arguments": {}, + }, + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("error", response) + + def test_execute_tool(self): + with freeze_time("2024-01-01"): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "tools/call", + "params": { + "name": "get_date", + "arguments": {}, + }, + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("result", response) + self.assertIn("structuredContent", response["result"]) + self.assertEqual(response["result"]["structuredContent"]["date"], "2024-01-01") + + def test_url(self): + self.server.key = "newkey" + self.assertEqual(self.server.url, "http://127.0.0.1:8069/mcp/newkey") + + def test_expiration_handling(self): + self.server.key_ids.write({"expiration_date": "2024-01-02 00:00:00"}) + with freeze_time("2024-01-01"): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertIn("result", response) + self.assertNotIn("error", response) + with freeze_time("2024-01-03"): + request = self.url_open( + f"/mcp/{self.server.key}", + data=json.dumps( + { + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + } + ), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.security_key}", + }, + ) + self.assertEqual(request.status_code, 200) + response = json.loads(request.content.decode("utf-8")) + self.assertNotIn("result", response) + self.assertIn("error", response) diff --git a/ai_oca_mcp/views/mcp_server.xml b/ai_oca_mcp/views/mcp_server.xml new file mode 100644 index 00000000..27f5be9f --- /dev/null +++ b/ai_oca_mcp/views/mcp_server.xml @@ -0,0 +1,92 @@ + + + + + + mcp.server + +
+
+
+ +
+
+ +
+
+ + + + + + + + + + + + +
+
+
+
+ + + mcp.server + + + + + + + + + mcp.server + + + + + + + + + MCP Server + mcp.server + tree,form + [] + {} + + + + MCP Server + + + + + +
diff --git a/ai_oca_mcp/views/mcp_server_key.xml b/ai_oca_mcp/views/mcp_server_key.xml new file mode 100644 index 00000000..54f4f41c --- /dev/null +++ b/ai_oca_mcp/views/mcp_server_key.xml @@ -0,0 +1,52 @@ + + + + + + mcp.server.key + +
+
+
+ + + + + + + + + +
+
+
+ + + mcp.server.key + + + + +
- {} new
- -
From 25651a5e733582448dbb59893a0eaf693e939b92 Mon Sep 17 00:00:00 2001 From: amoya Date: Fri, 5 Jun 2026 14:13:16 +0200 Subject: [PATCH 20/21] [MIG] ai_oca_mcp: Migration to 18.0 --- ai_oca_mcp/README.rst | 16 ++++------ ai_oca_mcp/__manifest__.py | 2 +- ai_oca_mcp/static/description/index.html | 36 ++++++++++------------- ai_oca_mcp/views/mcp_server.xml | 10 +++---- ai_oca_mcp/views/mcp_server_key.xml | 10 +++---- ai_oca_mcp/views/mcp_server_log.xml | 13 ++++---- ai_oca_mcp/wizards/mcp_server_key_add.xml | 8 ++--- 7 files changed, 38 insertions(+), 57 deletions(-) diff --git a/ai_oca_mcp/README.rst b/ai_oca_mcp/README.rst index 6954e8c5..2f5da1f8 100644 --- a/ai_oca_mcp/README.rst +++ b/ai_oca_mcp/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ========== Ai Oca Mcp ========== @@ -17,17 +13,17 @@ Ai Oca Mcp .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github - :target: https://github.com/OCA/ai/tree/16.0/ai_oca_mcp + :target: https://github.com/OCA/ai/tree/18.0/ai_oca_mcp :alt: OCA/ai .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_mcp + :target: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_oca_mcp :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -84,7 +80,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -116,6 +112,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/ai `_ project on GitHub. +This module is part of the `OCA/ai `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_oca_mcp/__manifest__.py b/ai_oca_mcp/__manifest__.py index 884735c1..c607a220 100644 --- a/ai_oca_mcp/__manifest__.py +++ b/ai_oca_mcp/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Ai Oca Mcp", "summary": """MCP Interface for Odoo""", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_oca_mcp/static/description/index.html b/ai_oca_mcp/static/description/index.html index 6c41bbc1..d795e6a7 100644 --- a/ai_oca_mcp/static/description/index.html +++ b/ai_oca_mcp/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Ai Oca Mcp -
+
+

Ai Oca Mcp

- - -Odoo Community Association - -
-

Ai Oca Mcp

-

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

This module exposes Odoo AI tools as an MCP (Model Context Protocol) server, allowing external AI clients such as n8n or custom agents to call Odoo functions via the standardized MCP protocol. Authentication is @@ -400,7 +395,7 @@

Ai Oca Mcp

-

Usage

+

Usage

  • Access in Developer mode
  • Go to AI > MCP Server
  • @@ -411,7 +406,7 @@

    Usage

  • Use the provided URL and the generated key to configure your AI client
-

Connecting from n8n

+

Connecting from n8n

Use the MCP node with:

-

Tool limitations

+

Tool limitations

Only tools of kind generic are supported. Tools requiring a record context (generic_model, record) cannot be used via MCP.

-

Security

+

Security

Each client should have its own API key. Keys can be expired individually from the server form or from AI > MCP Server Log to audit all calls.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -465,11 +460,10 @@

Maintainers

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/ai project on GitHub.

+

This module is part of the OCA/ai project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

-
diff --git a/ai_oca_mcp/views/mcp_server.xml b/ai_oca_mcp/views/mcp_server.xml index f9cf1c73..fd9b4331 100644 --- a/ai_oca_mcp/views/mcp_server.xml +++ b/ai_oca_mcp/views/mcp_server.xml @@ -15,7 +15,7 @@ type="action" class="btn-primary" icon="fa-key" - context="{'default_server_id': active_id}" + context="{'default_server_id': id}" />