biszx-odoo-mcp 1.1.0__py3-none-any.whl → 1.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,19 @@
1
+ """
2
+ MCP Server Application Context
3
+
4
+ This module defines the application context for the MCP server, which includes
5
+ the Odoo client used to interact with the Odoo server.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from biszx_odoo_mcp.tools.odoo_client import OdooClient
11
+
12
+
13
+ @dataclass
14
+ class AppContext:
15
+ """
16
+ Application context for the MCP server
17
+ """
18
+
19
+ odoo: OdooClient
@@ -0,0 +1,230 @@
1
+ """
2
+ MCP Resources for Odoo integration
3
+
4
+ This module contains all the MCP resource functions for Odoo data access.
5
+ """
6
+
7
+ from typing import Any, cast
8
+
9
+ from biszx_odoo_mcp.exceptions import OdooMCPError, ResourceError
10
+ from biszx_odoo_mcp.server.context import AppContext
11
+ from biszx_odoo_mcp.server.response import Response
12
+
13
+
14
+ async def search_models_resource(mcp: Any, query: str) -> str:
15
+ """
16
+ Resource for searching models from the Odoo application.
17
+
18
+ This searches through model names and display names to find models that
19
+ match the given query term.
20
+
21
+ Args:
22
+ query: Search term to find models (searches in model name and display name)
23
+
24
+ Returns:
25
+ JSON string with matching models
26
+ """
27
+ # Access lifespan context to get the Odoo client
28
+ ctx = mcp.get_context()
29
+ app_context = cast(AppContext, ctx.request_context.lifespan_context)
30
+
31
+ try:
32
+ data = app_context.odoo.search_models(query)
33
+ return Response(data=data).to_json_string()
34
+ except OdooMCPError as e:
35
+ return Response(error=e.to_dict()).to_json_string()
36
+ except Exception as e:
37
+ resource_error = ResourceError(
38
+ f"Unexpected error searching models: {str(e)}",
39
+ resource_name="search_models_resource",
40
+ details={"query": query},
41
+ original_error=e,
42
+ )
43
+ return Response(error=resource_error.to_dict()).to_json_string()
44
+
45
+
46
+ async def get_model_fields_resource(mcp: Any, model_name: str, query_field: str) -> str:
47
+ """
48
+ Resource containing field definitions for a specific model.
49
+
50
+ Args:
51
+ model_name: Name of the model (e.g., 'res.partner')
52
+ query_field: Search term to find fields (searches in field name and string)
53
+
54
+ Returns:
55
+ JSON string with field definitions
56
+ """
57
+ # Access lifespan context to get the Odoo client
58
+ ctx = mcp.get_context()
59
+ app_context = cast(AppContext, ctx.request_context.lifespan_context)
60
+
61
+ try:
62
+ data = app_context.odoo.get_model_fields(model_name, query_field)
63
+ return Response(data=data).to_json_string()
64
+ except OdooMCPError as e:
65
+ return Response(error=e.to_dict()).to_json_string()
66
+ except Exception as e:
67
+ resource_error = ResourceError(
68
+ f"Unexpected error getting model fields: {str(e)}",
69
+ resource_name="get_model_fields_resource",
70
+ details={"model_name": model_name},
71
+ original_error=e,
72
+ )
73
+ return Response(error=resource_error.to_dict()).to_json_string()
74
+
75
+
76
+ async def get_model_info_resource(mcp: Any, model_name: str) -> str:
77
+ """
78
+ Resource containing information about a specific model.
79
+
80
+ Args:
81
+ model_name: Name of the model (e.g., 'res.partner')
82
+
83
+ Returns:
84
+ JSON string with model information
85
+ """
86
+ # Access lifespan context to get the Odoo client
87
+ ctx = mcp.get_context()
88
+ app_context = cast(AppContext, ctx.request_context.lifespan_context)
89
+
90
+ try:
91
+ data = app_context.odoo.get_model_info(model_name)
92
+ return Response(data=data).to_json_string()
93
+ except OdooMCPError as e:
94
+ return Response(error=e.to_dict()).to_json_string()
95
+ except Exception as e:
96
+ resource_error = ResourceError(
97
+ f"Unexpected error getting model info: {str(e)}",
98
+ resource_name="get_model_info_resource",
99
+ details={"model_name": model_name},
100
+ original_error=e,
101
+ )
102
+ return Response(error=resource_error.to_dict()).to_json_string()
103
+
104
+
105
+ async def get_domain_help_resource() -> str:
106
+ """
107
+ Resource containing help information about Odoo domain syntax.
108
+
109
+ Returns:
110
+ JSON string with domain syntax examples and explanations
111
+ """
112
+ domain_help = {
113
+ "odoo_domain_syntax": {
114
+ "description": "Odoo domains are used for filtering records",
115
+ "syntax": "List of tuples: [('field', 'operator', 'value')]",
116
+ "operators": {
117
+ "=": "equals",
118
+ "!=": "not equals",
119
+ "<": "less than",
120
+ "<=": "less than or equal",
121
+ ">": "greater than",
122
+ ">=": "greater than or equal",
123
+ "in": "in list",
124
+ "not in": "not in list",
125
+ "like": "contains (case insensitive)",
126
+ "ilike": "contains (case insensitive)",
127
+ "=like": "matches pattern",
128
+ "=ilike": "matches pattern (case insensitive)",
129
+ },
130
+ "logical_operators": {
131
+ "&": "AND (default between conditions)",
132
+ "|": "OR",
133
+ "!": "NOT",
134
+ },
135
+ "examples": [
136
+ {
137
+ "description": "Find companies only",
138
+ "domain": "[('is_company', '=', True)]",
139
+ },
140
+ {
141
+ "description": "Find partners with email containing 'gmail'",
142
+ "domain": "[('email', 'ilike', 'gmail')]",
143
+ },
144
+ {
145
+ "description": "Find products with price between 10 and 100",
146
+ "domain": "[('list_price', '>=', 10), ('list_price', '<=', 100)]",
147
+ },
148
+ {
149
+ "description": "Find active products or services",
150
+ "domain": (
151
+ "['|', ('type', '=', 'product'), "
152
+ "('type', '=', 'service'), ('active', '=', True)]"
153
+ ),
154
+ },
155
+ {
156
+ "description": "Find draft or confirmed sales orders",
157
+ "domain": "['|', ('state', '=', 'draft'), ('state', '=', 'sent')]",
158
+ },
159
+ ],
160
+ }
161
+ }
162
+
163
+ response = Response(data=domain_help)
164
+ return response.to_json_string()
165
+
166
+
167
+ async def get_operations_help_resource() -> str:
168
+ """
169
+ Resource containing help information about available MCP tools and operations.
170
+
171
+ Returns:
172
+ JSON string with operations documentation
173
+ """
174
+ operations_help = {
175
+ "mcp_tools": {
176
+ "data_retrieval": {
177
+ "get_odoo_models": "Get list of all available models",
178
+ "get_model_info": "Get information about a specific model",
179
+ "get_model_fields": "Get field definitions for a model",
180
+ "search_records": "Search for records with domain filters",
181
+ "read_records": "Read specific records by IDs",
182
+ "search_ids": "Get only IDs of matching records",
183
+ "search_count": "Count records matching a domain",
184
+ },
185
+ "data_modification": {
186
+ "create_record": "Create a single new record",
187
+ "create_records": "Create multiple records at once",
188
+ "write_record": "Update a single record",
189
+ "write_records": "Update multiple records",
190
+ "unlink_record": "Delete a single record",
191
+ "unlink_records": "Delete multiple records",
192
+ },
193
+ "advanced": {
194
+ "call_method": "Call custom methods on models",
195
+ "search_and_update": "Search and update records in one operation",
196
+ },
197
+ },
198
+ "common_workflows": [
199
+ {
200
+ "name": "Create a new customer",
201
+ "steps": [
202
+ "1. Use create_record with model 'res.partner'",
203
+ (
204
+ "2. Provide values like {'name': 'Customer Name', "
205
+ "'email': 'email@domain.com', 'is_company': True}"
206
+ ),
207
+ ],
208
+ },
209
+ {
210
+ "name": "Search for products",
211
+ "steps": [
212
+ "1. Use search_records with model 'product.template'",
213
+ (
214
+ "2. Use domain like [('name', 'ilike', 'search_term')] "
215
+ "to find products by name"
216
+ ),
217
+ ],
218
+ },
219
+ {
220
+ "name": "Update product price",
221
+ "steps": [
222
+ "1. Use search_ids to find the product",
223
+ "2. Use write_records to update the list_price field",
224
+ ],
225
+ },
226
+ ],
227
+ }
228
+
229
+ response = Response(data=operations_help)
230
+ return response.to_json_string()
@@ -0,0 +1,36 @@
1
+ """
2
+ Odoo MCP Server Response
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Optional
7
+
8
+
9
+ class Response:
10
+ """
11
+ Standard response wrapper for OdooClient methods.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ data: Optional[Any] = None,
17
+ error: Optional[dict[str, Any]] = None,
18
+ ) -> None:
19
+ self.data = data
20
+ self.error = error
21
+ self.success = error is None
22
+
23
+ def to_dict(self) -> dict[str, Any]:
24
+ """
25
+ Return the response as a dictionary with either 'data' or 'error' key.
26
+ """
27
+ if self.error is not None:
28
+ return {"success": self.success, "error": self.error}
29
+ return {"success": self.success, "data": self.data}
30
+
31
+ def to_json_string(self, indent: int = 2) -> str:
32
+ """
33
+ Return the response as a JSON string.
34
+ """
35
+
36
+ return json.dumps(self.to_dict(), indent=indent)