diff --git a/ai_oca_mcp/README.rst b/ai_oca_mcp/README.rst new file mode 100644 index 0000000..314fa00 --- /dev/null +++ b/ai_oca_mcp/README.rst @@ -0,0 +1,121 @@ +.. 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 +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:77c677f59d0b6ca70acba28eabf1fd8140521f9c15b7093e7f3fe2a7c173ad32 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_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 + :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 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** + +.. contents:: + :local: + +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 +------------ + +- `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_oca_mcp/__init__.py b/ai_oca_mcp/__init__.py new file mode 100644 index 0000000..ada0b87 --- /dev/null +++ b/ai_oca_mcp/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import wizards diff --git a/ai_oca_mcp/__manifest__.py b/ai_oca_mcp/__manifest__.py new file mode 100644 index 0000000..884735c --- /dev/null +++ b/ai_oca_mcp/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Ai Oca Mcp", + "summary": """MCP Interface for Odoo""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/ai", + "depends": [ + "ai_tool", + ], + "data": [ + "views/mcp_server_log.xml", + "security/ir.model.access.csv", + "views/mcp_server_key.xml", + "wizards/mcp_server_key_add.xml", + "views/mcp_server.xml", + ], + "demo": [], +} diff --git a/ai_oca_mcp/controllers/__init__.py b/ai_oca_mcp/controllers/__init__.py new file mode 100644 index 0000000..350e212 --- /dev/null +++ b/ai_oca_mcp/controllers/__init__.py @@ -0,0 +1 @@ +from . import mcp_controller diff --git a/ai_oca_mcp/controllers/mcp_controller.py b/ai_oca_mcp/controllers/mcp_controller.py new file mode 100644 index 0000000..f90ac8b --- /dev/null +++ b/ai_oca_mcp/controllers/mcp_controller.py @@ -0,0 +1,71 @@ +import json +import re + +from odoo import fields, http +from odoo.http import request + + +class McpController(http.Controller): + @http.route( + "/mcp/", type="http", auth="none", methods=["POST"], csrf=False + ) + def mcp_endpoint(self, key, **kwargs): + match = re.match( + r"Bearer (.+)", request.httprequest.headers.get("Authorization", "") + ) + payload = json.loads(request.httprequest.data.decode("utf-8")) + if not match: + return request.make_json_response( + { + "jsonrpc": "2.0", + "id": payload.get("id"), + "error": {"code": -32000, "message": "Connection failed"}, + }, + status=401, + ) + security_key = match.group(1).strip() + server_id, expiration_date = ( + request.env["mcp.server.key"] + .sudo() + ._get_mcp_server_by_key(key, security_key) + ) + if expiration_date and expiration_date < fields.Datetime.now(): + request.env["mcp.server.key"].sudo().browse(server_id).expire_key() + server_id = False + if not server_id: + return request.make_json_response( + { + "jsonrpc": "2.0", + "id": payload.get("id"), + "error": {"code": -32000, "message": "Connection failed"}, + } + ) + server = request.env["mcp.server.key"].sudo().browse(server_id) + server = server.with_user(server.user_id.id) + method = payload.get("method") + if method == "initialize": + return request.make_json_response( + { + "jsonrpc": "2.0", + "id": payload.get("id"), + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {"listChanged": True}}, + "serverInfo": {"name": "odoo-mcp", "version": "0.1.0"}, + }, + } + ) + + if method == "tools/list": + return request.make_json_response(server._tools_list(payload)) + + if method == "tools/call": + return request.make_json_response(server._tools_call(payload)) + + return request.make_json_response( + { + "jsonrpc": "2.0", + "id": payload.get("id"), + "error": {"code": -32601, "message": "Method not found"}, + } + ) diff --git a/ai_oca_mcp/models/__init__.py b/ai_oca_mcp/models/__init__.py new file mode 100644 index 0000000..9370d8c --- /dev/null +++ b/ai_oca_mcp/models/__init__.py @@ -0,0 +1,3 @@ +from . import mcp_server +from . import mcp_server_key +from . import mcp_server_log diff --git a/ai_oca_mcp/models/mcp_server.py b/ai_oca_mcp/models/mcp_server.py new file mode 100644 index 0000000..adff795 --- /dev/null +++ b/ai_oca_mcp/models/mcp_server.py @@ -0,0 +1,40 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from uuid import uuid4 + +from odoo import api, fields, models + + +class McpServer(models.Model): + + _name = "mcp.server" + _description = "Mcp Server" + + name = fields.Char() + description = fields.Text() + active = fields.Boolean(default=True) + key = fields.Char( + required=True, + copy=False, + default=lambda self: uuid4(), + groups="base.group_system", + ) + tool_ids = fields.Many2many( + "ai.tool", string="Tools", domain=[("kind", "=", "generic")] + ) + url = fields.Char( + compute="_compute_url", + groups="base.group_system", + ) + key_ids = fields.One2many("mcp.server.key", "server_id", string="Access Keys") + + _sql_constraints = [ + ("key_uniq", "unique(key)", "The key must be unique"), + ] + + @api.depends("key") + def _compute_url(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for record in self: + record.url = f"{base_url}/mcp/{record.key}" diff --git a/ai_oca_mcp/models/mcp_server_key.py b/ai_oca_mcp/models/mcp_server_key.py new file mode 100644 index 0000000..a40442a --- /dev/null +++ b/ai_oca_mcp/models/mcp_server_key.py @@ -0,0 +1,105 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +from hashlib import sha256 + +from odoo import api, fields, models, tools + + +class McpServerKey(models.Model): + + _name = "mcp.server.key" + _description = "Mcp Server Key" + + name = fields.Char(required=True) + server_id = fields.Many2one("mcp.server", required=True, ondelete="cascade") + hashed_key = fields.Char(copy=False) + state = fields.Selection( + [("active", "Active"), ("expired", "Expired")], default="active" + ) + user_id = fields.Many2one("res.users", default=lambda self: self.env.user) + expiration_date = fields.Datetime() + expired_on = fields.Datetime(readonly=True) + + def expire_key(self): + self.filtered(lambda key: key.state == "active").write( + { + "state": "expired", + "expired_on": fields.Datetime.now(), + } + ) + self._get_mcp_server_by_key.clear_cache(self) + + _sql_constraints = [ + ("key_uniq", "unique(hashed_key)", "The key must be unique"), + ] + + @api.model + def _hash_key(self, key): + return sha256(key.encode()).hexdigest() + + @tools.ormcache("key", "security_key") + def _get_mcp_server_by_key(self, key, security_key): + key = self.sudo().search( + [ + ("server_id.key", "=", key), + ("server_id.active", "=", True), + ("hashed_key", "=", self._hash_key(security_key)), + "|", + ("expiration_date", "=", False), + ("expiration_date", ">", fields.Datetime.now()), + ("state", "=", "active"), + ], + limit=1, + ) + return (key.id, key.expiration_date) if key else (False, False) + + def _tools_list(self, payload): + tools = self.server_id.tool_ids + result = [] + for tool in tools: + result.append(tool._get_tool_definition()) + return {"jsonrpc": "2.0", "id": payload.get("id"), "result": {"tools": result}} + + def _tools_call(self, payload): + params = payload.get("params", {}) + tool_name = params.get("name") + args = params.get("arguments", {}) + result_vals = { + "request": json.dumps(params), + "server_id": self.server_id.id, + "server_key_id": self.id, + } + + tool = self.server_id.tool_ids.filtered(lambda t: t.name == tool_name) + + if not tool: + self._add_log(**result_vals, error="Tool not found") + return { + "jsonrpc": "2.0", + "id": payload.get("id"), + "error": {"code": -32000, "message": "Unknown tool"}, + } + try: + with self.env.cr.savepoint(flush=False): + result = tool._execute_tool(**args) or {} + self._add_log(**result_vals, response=json.dumps(result)) + return { + "jsonrpc": "2.0", + "id": payload.get("id"), + "result": { + "structuredContent": result, + "content": [{"type": "text", "text": json.dumps(result)}], + }, + } + except Exception as e: + self._add_log(**result_vals, error=str(e)) + return { + "jsonrpc": "2.0", + "id": payload.get("id"), + "error": {"code": -32000, "message": str(e)}, + } + + def _add_log(self, **log_vals): + return self.env["mcp.server.log"].sudo().create(log_vals) diff --git a/ai_oca_mcp/models/mcp_server_log.py b/ai_oca_mcp/models/mcp_server_log.py new file mode 100644 index 0000000..5bd6103 --- /dev/null +++ b/ai_oca_mcp/models/mcp_server_log.py @@ -0,0 +1,16 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class McpServerLog(models.Model): + _name = "mcp.server.log" + _description = "Mcp Server Log" + + server_id = fields.Many2one("mcp.server", required=True, ondelete="cascade") + server_key_id = fields.Many2one("mcp.server.key", required=True, ondelete="cascade") + request = fields.Text() + response = fields.Text() + error = fields.Text() + date = fields.Datetime(default=fields.Datetime.now, index=True) diff --git a/ai_oca_mcp/readme/CONTRIBUTORS.md b/ai_oca_mcp/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..2c066ba --- /dev/null +++ b/ai_oca_mcp/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/ai_oca_mcp/readme/DESCRIPTION.md b/ai_oca_mcp/readme/DESCRIPTION.md new file mode 100644 index 0000000..a7eb05e --- /dev/null +++ b/ai_oca_mcp/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +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. diff --git a/ai_oca_mcp/readme/USAGE.md b/ai_oca_mcp/readme/USAGE.md new file mode 100644 index 0000000..4a4f37e --- /dev/null +++ b/ai_oca_mcp/readme/USAGE.md @@ -0,0 +1,19 @@ +- 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. diff --git a/ai_oca_mcp/security/ir.model.access.csv b/ai_oca_mcp/security/ir.model.access.csv new file mode 100644 index 0000000..cf664cd --- /dev/null +++ b/ai_oca_mcp/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mcp_server,access_mcp_server,model_mcp_server,base.group_system,1,1,1,1 +access_mcp_server_key,access_mcp_server_key,model_mcp_server_key,base.group_system,1,1,1,1 +access_mcp_server_key_add,access_mcp_server_key_add,model_mcp_server_key_add,base.group_system,1,1,1,1 +access_mcp_server_log,access_mcp_server_log,model_mcp_server_log,base.group_system,1,0,0,0 diff --git a/ai_oca_mcp/static/description/icon.png b/ai_oca_mcp/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/ai_oca_mcp/static/description/icon.png differ diff --git a/ai_oca_mcp/static/description/index.html b/ai_oca_mcp/static/description/index.html new file mode 100644 index 0000000..b127fde --- /dev/null +++ b/ai_oca_mcp/static/description/index.html @@ -0,0 +1,475 @@ + + + + + +README.rst + + + +
+ + + +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 0000000..cb03ee1 --- /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 0000000..15e3e45 --- /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 0000000..27f5be9 --- /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 0000000..54f4f41 --- /dev/null +++ b/ai_oca_mcp/views/mcp_server_key.xml @@ -0,0 +1,52 @@ + + + + + + mcp.server.key + +
+
+
+ + + + + + + + + +
+
+
+ + + mcp.server.key + + + + +