d365fo-client 0.1.0__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.
- d365fo_client/__init__.py +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,579 @@
|
|
1
|
+
"""CRUD operation tools for MCP server."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
from typing import List
|
7
|
+
|
8
|
+
from mcp import Tool
|
9
|
+
from mcp.types import TextContent
|
10
|
+
|
11
|
+
from ...models import QueryOptions
|
12
|
+
from ..client_manager import D365FOClientManager
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class CrudTools:
|
18
|
+
"""CRUD operation tools for the MCP server."""
|
19
|
+
|
20
|
+
def __init__(self, client_manager: D365FOClientManager):
|
21
|
+
"""Initialize CRUD tools.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
client_manager: D365FO client manager instance
|
25
|
+
"""
|
26
|
+
self.client_manager = client_manager
|
27
|
+
|
28
|
+
def get_tools(self) -> List[Tool]:
|
29
|
+
"""Get list of CRUD tools.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
List of Tool definitions
|
33
|
+
"""
|
34
|
+
return [
|
35
|
+
self._get_query_entities_tool(),
|
36
|
+
self._get_entity_record_tool(),
|
37
|
+
self._get_create_record_tool(),
|
38
|
+
self._get_update_record_tool(),
|
39
|
+
self._get_delete_record_tool(),
|
40
|
+
self._get_call_action_tool(),
|
41
|
+
]
|
42
|
+
|
43
|
+
def _get_query_entities_tool(self) -> Tool:
|
44
|
+
"""Get query entities tool definition."""
|
45
|
+
return Tool(
|
46
|
+
name="d365fo_query_entities",
|
47
|
+
description="Query and retrieve multiple records from D365 Finance & Operations data entities using OData standards. Supports advanced filtering, sorting, field selection, and pagination. Use this tool to search for specific records, generate reports, or perform bulk data analysis. Returns structured JSON data with optional metadata like record counts and pagination links.",
|
48
|
+
inputSchema={
|
49
|
+
"type": "object",
|
50
|
+
"properties": {
|
51
|
+
"entityName": {
|
52
|
+
"type": "string",
|
53
|
+
"description": "The name of the D365FO data entity to query. This should be the public collection name or the entity set name (e.g., 'CustomersV3', 'SalesOrderHeadersV2', 'ItemsV2'). Use metadata discovery tools first to find the correct entity name if unsure.",
|
54
|
+
},
|
55
|
+
"profile": {
|
56
|
+
"type": "string",
|
57
|
+
"description": "Configuration profile name containing connection details and authentication settings. Use 'default' if not specified or when working with a single environment.",
|
58
|
+
"default": "default",
|
59
|
+
},
|
60
|
+
"select": {
|
61
|
+
"type": "array",
|
62
|
+
"items": {"type": "string"},
|
63
|
+
"description": "Array of field names to include in the response (OData $select). Only specified fields will be returned, improving performance and reducing payload size. Example: ['CustomerAccount', 'Name', 'PrimaryContactEmail']. If omitted, all fields are returned.",
|
64
|
+
},
|
65
|
+
"filter": {
|
66
|
+
"type": "string",
|
67
|
+
"description": 'OData filter expression to restrict which records are returned (OData $filter). Supports standard OData operators: eq (equals), ne (not equals), gt (greater than), ge (greater or equal), lt (less than), le (less or equal), and (), or, not. Examples: "CustomerAccount eq \'CUST001\'", "CreditLimit gt 10000", "Name contains \'Corp\'".',
|
68
|
+
},
|
69
|
+
"expand": {
|
70
|
+
"type": "array",
|
71
|
+
"items": {"type": "string"},
|
72
|
+
"description": "Array of navigation property names to expand and include related entity data (OData $expand). This fetches related records in a single request. Example: ['PrimaryAddress', 'SalesOrders']. Use sparingly as it increases response size and processing time.",
|
73
|
+
},
|
74
|
+
"orderBy": {
|
75
|
+
"type": "array",
|
76
|
+
"items": {"type": "string"},
|
77
|
+
"description": "Array of field names for sorting results (OData $orderby). Add 'desc' suffix for descending order. Examples: ['Name'], ['CreditLimit desc'], ['CustomerAccount', 'Name desc']. Default order is ascending.",
|
78
|
+
},
|
79
|
+
"top": {
|
80
|
+
"type": "integer",
|
81
|
+
"minimum": 1,
|
82
|
+
"maximum": 1000,
|
83
|
+
"description": "Maximum number of records to return (OData $top). Use for pagination and performance optimization. D365FO has built-in limits, typically 1000 records maximum per request. Combine with 'skip' for pagination.",
|
84
|
+
},
|
85
|
+
"skip": {
|
86
|
+
"type": "integer",
|
87
|
+
"minimum": 0,
|
88
|
+
"description": "Number of records to skip before returning results (OData $skip). Used for pagination - skip N records to get the next page. Example: skip=100 with top=50 gets records 101-150.",
|
89
|
+
},
|
90
|
+
"count": {
|
91
|
+
"type": "boolean",
|
92
|
+
"description": "Whether to include the total count of matching records in the response (OData $count). Useful for pagination UI and progress indicators. May impact performance on large datasets as it requires counting all matching records.",
|
93
|
+
},
|
94
|
+
},
|
95
|
+
"required": ["entityName"],
|
96
|
+
},
|
97
|
+
)
|
98
|
+
|
99
|
+
def _get_entity_record_tool(self) -> Tool:
|
100
|
+
"""Get entity record tool definition."""
|
101
|
+
return Tool(
|
102
|
+
name="d365fo_get_entity_record",
|
103
|
+
description="Retrieve a single specific record from a D365 Finance & Operations data entity using its primary key. This is the most efficient way to fetch a known record when you have its unique identifier. Supports field selection and expanding related data. Use this when you need details for one specific record rather than searching through multiple records.",
|
104
|
+
inputSchema={
|
105
|
+
"type": "object",
|
106
|
+
"properties": {
|
107
|
+
"entityName": {
|
108
|
+
"type": "string",
|
109
|
+
"description": "The name of the D365FO data entity containing the record. This should be the public entity name (e.g., 'CustomersV3', 'SalesOrderHeadersV2'). Use metadata discovery tools to find the correct entity name and identify key fields.",
|
110
|
+
},
|
111
|
+
"key": {
|
112
|
+
"oneOf": [
|
113
|
+
{
|
114
|
+
"type": "string",
|
115
|
+
"description": "Single string key value for entities with simple primary keys",
|
116
|
+
},
|
117
|
+
{
|
118
|
+
"type": "object",
|
119
|
+
"description": "Composite key object for entities with multiple key fields",
|
120
|
+
"additionalProperties": {"type": "string"},
|
121
|
+
},
|
122
|
+
],
|
123
|
+
"description": "Primary key value(s) that uniquely identify the record. For single-key entities, provide a string value (e.g., 'CUST001'). For composite-key entities, provide an object with key field names and values (e.g., {'DataArea': 'USMF', 'CustomerAccount': 'CUST001'}). Use entity schema discovery to identify key fields.",
|
124
|
+
},
|
125
|
+
"select": {
|
126
|
+
"type": "array",
|
127
|
+
"items": {"type": "string"},
|
128
|
+
"description": "Array of specific field names to include in the response (OData $select). Only specified fields will be returned, improving performance and reducing response size. Example: ['CustomerAccount', 'Name', 'PrimaryContactEmail']. If omitted, all fields are returned.",
|
129
|
+
},
|
130
|
+
"expand": {
|
131
|
+
"type": "array",
|
132
|
+
"items": {"type": "string"},
|
133
|
+
"description": "Array of navigation property names to expand and include related entity data in the response (OData $expand). This allows fetching related records in a single request. Example: ['PrimaryAddress', 'ContactDetails']. Use entity schema discovery to identify available navigation properties.",
|
134
|
+
},
|
135
|
+
"profile": {
|
136
|
+
"type": "string",
|
137
|
+
"description": "Configuration profile to use (optional - uses default profile if not specified)",
|
138
|
+
},
|
139
|
+
},
|
140
|
+
"required": ["entityName", "key"],
|
141
|
+
},
|
142
|
+
)
|
143
|
+
|
144
|
+
def _get_create_record_tool(self) -> Tool:
|
145
|
+
"""Get create record tool definition."""
|
146
|
+
return Tool(
|
147
|
+
name="d365fo_create_entity_record",
|
148
|
+
description="Create a new record in a D365 Finance & Operations data entity. This operation performs data validation and may trigger business logic, workflows, and number sequences. The entity must support write operations (not read-only). Use entity schema discovery to identify required fields, data types, and validation rules before creating records.",
|
149
|
+
inputSchema={
|
150
|
+
"type": "object",
|
151
|
+
"properties": {
|
152
|
+
"entityName": {
|
153
|
+
"type": "string",
|
154
|
+
"description": "The name of the D365FO data entity where the new record will be created. This should be the public collection name or the entity set name (e.g., 'CustomersV3', 'SalesOrderHeadersV2'). Verify the entity supports create operations by checking it's not read-only using metadata discovery tools.",
|
155
|
+
},
|
156
|
+
"data": {
|
157
|
+
"type": "object",
|
158
|
+
"description": "Record data object containing field names and values for the new record. Must include all mandatory fields as defined in the entity schema. Field names are case-sensitive and must match the entity's property names exactly. Example: {'CustomerAccount': 'CUST001', 'Name': 'Example Corp', 'CustomerGroupId': 'DEFAULT'}. Use entity schema discovery to identify required fields and their data types.",
|
159
|
+
},
|
160
|
+
"returnRecord": {
|
161
|
+
"type": "boolean",
|
162
|
+
"description": "Whether to return the complete created record in the response. Set to true to get the full record with system-generated values (like IDs, timestamps, calculated fields). Set to false for better performance when you only need confirmation of creation success.",
|
163
|
+
"default": False,
|
164
|
+
},
|
165
|
+
"profile": {
|
166
|
+
"type": "string",
|
167
|
+
"description": "Configuration profile to use (optional - uses default profile if not specified)",
|
168
|
+
},
|
169
|
+
},
|
170
|
+
"required": ["entityName", "data"],
|
171
|
+
},
|
172
|
+
)
|
173
|
+
|
174
|
+
def _get_update_record_tool(self) -> Tool:
|
175
|
+
"""Get update record tool definition."""
|
176
|
+
return Tool(
|
177
|
+
name="d365fo_update_entity_record",
|
178
|
+
description="Update an existing record in a D365 Finance & Operations data entity. This operation modifies specific fields while preserving others, performs validation, and may trigger business logic and workflows. The entity must support write operations. Use partial updates by including only the fields you want to change - omitted fields remain unchanged.",
|
179
|
+
inputSchema={
|
180
|
+
"type": "object",
|
181
|
+
"properties": {
|
182
|
+
"entityName": {
|
183
|
+
"type": "string",
|
184
|
+
"description": "The name of the D365FO data entity containing the record to update. This should be the public entity name (e.g., 'CustomersV3', 'SalesOrderHeadersV2'). Verify the entity supports update operations by checking it's not read-only using metadata discovery tools.",
|
185
|
+
},
|
186
|
+
"key": {
|
187
|
+
"oneOf": [
|
188
|
+
{
|
189
|
+
"type": "string",
|
190
|
+
"description": "Single string key value for entities with simple primary keys",
|
191
|
+
},
|
192
|
+
{
|
193
|
+
"type": "object",
|
194
|
+
"description": "Composite key object for entities with multiple key fields",
|
195
|
+
"properties": {},
|
196
|
+
},
|
197
|
+
],
|
198
|
+
"description": "Primary key value(s) that uniquely identify the record to update. For single-key entities, provide a string value (e.g., 'CUST001'). For composite-key entities, provide an object with key field names and values (e.g., {'DataArea': 'USMF', 'CustomerAccount': 'CUST001'}). The record must exist or the operation will fail.",
|
199
|
+
},
|
200
|
+
"data": {
|
201
|
+
"type": "object",
|
202
|
+
"description": "Record data object containing only the fields and values to update. This is a partial update - only include fields you want to change. Field names are case-sensitive and must match the entity's property names exactly. Example: {'Name': 'Updated Corp Name', 'CreditLimit': 50000}. Key fields typically cannot be updated.",
|
203
|
+
},
|
204
|
+
"returnRecord": {
|
205
|
+
"type": "boolean",
|
206
|
+
"description": "Whether to return the complete updated record in the response. Set to true to get the full record with all current values after the update. Set to false for better performance when you only need confirmation of update success.",
|
207
|
+
"default": False,
|
208
|
+
},
|
209
|
+
"ifMatch": {
|
210
|
+
"type": "string",
|
211
|
+
"description": "ETag value for optimistic concurrency control (optional). If provided, the update will only succeed if the record hasn't been modified by another process since the ETag was obtained. This prevents conflicting updates in multi-user scenarios. Get the ETag from a previous read operation.",
|
212
|
+
},
|
213
|
+
"profile": {
|
214
|
+
"type": "string",
|
215
|
+
"description": "Configuration profile to use (optional - uses default profile if not specified)",
|
216
|
+
},
|
217
|
+
},
|
218
|
+
"required": ["entityName", "key", "data"],
|
219
|
+
},
|
220
|
+
)
|
221
|
+
|
222
|
+
def _get_delete_record_tool(self) -> Tool:
|
223
|
+
"""Get delete record tool definition."""
|
224
|
+
return Tool(
|
225
|
+
name="d365fo_delete_entity_record",
|
226
|
+
description="Permanently delete a record from a D365 Finance & Operations data entity. This operation removes the record completely and may trigger cascading deletes, business logic, and workflows. The entity must support delete operations. Use with caution as this action cannot be undone. Verify business rules allow deletion before proceeding.",
|
227
|
+
inputSchema={
|
228
|
+
"type": "object",
|
229
|
+
"properties": {
|
230
|
+
"entityName": {
|
231
|
+
"type": "string",
|
232
|
+
"description": "The name of the D365FO data entity containing the record to delete. This should be the public entity name (e.g., 'CustomersV3', 'SalesOrderHeadersV2'). Verify the entity supports delete operations and check for any business constraints that might prevent deletion.",
|
233
|
+
},
|
234
|
+
"key": {
|
235
|
+
"oneOf": [
|
236
|
+
{
|
237
|
+
"type": "string",
|
238
|
+
"description": "Single string key value for entities with simple primary keys",
|
239
|
+
},
|
240
|
+
{
|
241
|
+
"type": "object",
|
242
|
+
"description": "Composite key object for entities with multiple key fields",
|
243
|
+
"properties": {},
|
244
|
+
},
|
245
|
+
],
|
246
|
+
"description": "Primary key value(s) that uniquely identify the record to delete. For single-key entities, provide a string value (e.g., 'CUST001'). For composite-key entities, provide an object with key field names and values (e.g., {'DataArea': 'USMF', 'CustomerAccount': 'CUST001'}). The record must exist or the operation will fail.",
|
247
|
+
},
|
248
|
+
"ifMatch": {
|
249
|
+
"type": "string",
|
250
|
+
"description": "ETag value for optimistic concurrency control (optional). If provided, the delete will only succeed if the record hasn't been modified by another process since the ETag was obtained. This prevents accidental deletion of records that have been updated by other users. Get the ETag from a previous read operation.",
|
251
|
+
},
|
252
|
+
"profile": {
|
253
|
+
"type": "string",
|
254
|
+
"description": "Configuration profile to use (optional - uses default profile if not specified)",
|
255
|
+
},
|
256
|
+
},
|
257
|
+
"required": ["entityName", "key"],
|
258
|
+
},
|
259
|
+
)
|
260
|
+
|
261
|
+
def _get_call_action_tool(self) -> Tool:
|
262
|
+
"""Get call action tool definition."""
|
263
|
+
return Tool(
|
264
|
+
name="d365fo_call_action",
|
265
|
+
description="Execute/invoke a D365 Finance & Operations OData action method. Actions are server-side operations that perform business logic, calculations, or complex operations beyond standard CRUD. Actions can be unbound (standalone), bound to entity collections, or bound to specific entity instances. Use action discovery tools to find available actions and their parameters before calling.",
|
266
|
+
inputSchema={
|
267
|
+
"type": "object",
|
268
|
+
"properties": {
|
269
|
+
"actionName": {
|
270
|
+
"type": "string",
|
271
|
+
"description": "The full name of the OData action to invoke. This is typically in the format 'Microsoft.Dynamics.DataEntities.ActionName' for system actions, or a simple name for custom actions. Examples: 'Microsoft.Dynamics.DataEntities.GetKeys', 'Microsoft.Dynamics.DataEntities.GetApplicationVersion'. Use action search tools to discover available actions and their exact names.",
|
272
|
+
},
|
273
|
+
"profile": {
|
274
|
+
"type": "string",
|
275
|
+
"description": "Configuration profile name containing connection details and authentication settings for the D365FO environment. Use 'default' if not specified or when working with a single environment. Different profiles allow connecting to multiple D365FO environments (dev, test, prod).",
|
276
|
+
"default": "default",
|
277
|
+
},
|
278
|
+
"parameters": {
|
279
|
+
"type": "object",
|
280
|
+
"description": "Action parameters as key-value pairs (optional). Parameter names and types must match the action definition exactly. Use action discovery tools to identify required and optional parameters. Examples: {'entityName': 'CustomersV3'}, {'startDate': '2024-01-01', 'endDate': '2024-12-31'}. Leave empty {} for actions that require no parameters.",
|
281
|
+
},
|
282
|
+
"entityName": {
|
283
|
+
"type": "string",
|
284
|
+
"description": "The name of the data entity for entity-bound actions (optional). Required for actions with bindingKind 'BoundToEntitySet' or 'BoundToEntity'. This should be the public entity name or collection name. Use metadata discovery to identify the correct entity name for bound actions.",
|
285
|
+
},
|
286
|
+
"entityKey": {
|
287
|
+
"oneOf": [
|
288
|
+
{
|
289
|
+
"type": "string",
|
290
|
+
"description": "Single string key value for simple primary keys",
|
291
|
+
},
|
292
|
+
{
|
293
|
+
"type": "object",
|
294
|
+
"description": "Composite key object for multiple key fields",
|
295
|
+
"properties": {},
|
296
|
+
},
|
297
|
+
],
|
298
|
+
"description": "Primary key value(s) identifying a specific entity instance for 'BoundToEntity' actions (optional). For single-key entities, provide a string. For composite keys, provide an object with key field names and values. Only required when bindingKind is 'BoundToEntity'.",
|
299
|
+
},
|
300
|
+
"bindingKind": {
|
301
|
+
"type": "string",
|
302
|
+
"description": "Explicitly specify the action's binding type if known (optional). Helps the system determine how to invoke the action. 'Unbound' actions are called directly. 'BoundToEntitySet' actions operate on entity collections. 'BoundToEntity' actions operate on specific entity instances. If not provided, the system will attempt to determine the binding automatically.",
|
303
|
+
"enum": ["Unbound", "BoundToEntitySet", "BoundToEntity"],
|
304
|
+
},
|
305
|
+
"timeout": {
|
306
|
+
"type": "integer",
|
307
|
+
"minimum": 1,
|
308
|
+
"maximum": 300,
|
309
|
+
"default": 30,
|
310
|
+
"description": "Request timeout in seconds. Actions may take longer than normal CRUD operations, especially complex business calculations or batch operations. Increase timeout for long-running actions. Default is 30 seconds, maximum is 300 seconds (5 minutes).",
|
311
|
+
},
|
312
|
+
},
|
313
|
+
"required": ["actionName"],
|
314
|
+
},
|
315
|
+
)
|
316
|
+
|
317
|
+
async def execute_query_entities(self, arguments: dict) -> List[TextContent]:
|
318
|
+
"""Execute query entities tool.
|
319
|
+
|
320
|
+
Args:
|
321
|
+
arguments: Tool arguments
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
List of TextContent responses
|
325
|
+
"""
|
326
|
+
try:
|
327
|
+
profile = arguments.get("profile", "default")
|
328
|
+
client = await self.client_manager.get_client(profile)
|
329
|
+
|
330
|
+
# Build query options
|
331
|
+
options = QueryOptions(
|
332
|
+
select=arguments.get("select"),
|
333
|
+
filter=arguments.get("filter"),
|
334
|
+
expand=arguments.get("expand"),
|
335
|
+
orderby=arguments.get("orderBy"),
|
336
|
+
top=arguments.get("top", None),
|
337
|
+
skip=arguments.get("skip"),
|
338
|
+
count=arguments.get("count", False),
|
339
|
+
)
|
340
|
+
|
341
|
+
# Execute query
|
342
|
+
start_time = time.time()
|
343
|
+
result = await client.get_entities(arguments["entityName"], options=options)
|
344
|
+
query_time = time.time() - start_time
|
345
|
+
|
346
|
+
# Format response
|
347
|
+
response = {
|
348
|
+
"data": result.get("value", []),
|
349
|
+
"count": result.get("@odata.count"),
|
350
|
+
"nextLink": result.get("@odata.nextLink"),
|
351
|
+
"queryTime": round(query_time, 3),
|
352
|
+
"totalRecords": len(result.get("value", [])),
|
353
|
+
}
|
354
|
+
|
355
|
+
return [TextContent(type="text", text=json.dumps(response, indent=2))]
|
356
|
+
|
357
|
+
except Exception as e:
|
358
|
+
logger.error(f"Query entities failed: {e}")
|
359
|
+
error_response = {
|
360
|
+
"error": str(e),
|
361
|
+
"tool": "d365fo_query_entities",
|
362
|
+
"arguments": arguments,
|
363
|
+
}
|
364
|
+
return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
|
365
|
+
|
366
|
+
async def execute_get_entity_record(self, arguments: dict) -> List[TextContent]:
|
367
|
+
"""Execute get entity record tool.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
arguments: Tool arguments
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
List of TextContent responses
|
374
|
+
"""
|
375
|
+
try:
|
376
|
+
profile = arguments.get("profile", "default")
|
377
|
+
client = await self.client_manager.get_client(profile)
|
378
|
+
|
379
|
+
start_time = time.time()
|
380
|
+
record = await client.get_entity_by_key(
|
381
|
+
arguments["entityName"],
|
382
|
+
arguments["key"],
|
383
|
+
select=arguments.get("select"),
|
384
|
+
expand=arguments.get("expand"),
|
385
|
+
)
|
386
|
+
retrieval_time = time.time() - start_time
|
387
|
+
|
388
|
+
response = {
|
389
|
+
"record": record,
|
390
|
+
"found": record is not None,
|
391
|
+
"retrievalTime": round(retrieval_time, 3),
|
392
|
+
}
|
393
|
+
|
394
|
+
return [TextContent(type="text", text=json.dumps(response, indent=2))]
|
395
|
+
|
396
|
+
except Exception as e:
|
397
|
+
logger.error(f"Get entity record failed: {e}")
|
398
|
+
error_response = {
|
399
|
+
"error": str(e),
|
400
|
+
"tool": "d365fo_get_entity_record",
|
401
|
+
"arguments": arguments,
|
402
|
+
}
|
403
|
+
return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
|
404
|
+
|
405
|
+
async def execute_create_entity_record(self, arguments: dict) -> List[TextContent]:
|
406
|
+
"""Execute create entity record tool.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
arguments: Tool arguments
|
410
|
+
|
411
|
+
Returns:
|
412
|
+
List of TextContent responses
|
413
|
+
"""
|
414
|
+
try:
|
415
|
+
profile = arguments.get("profile", "default")
|
416
|
+
client = await self.client_manager.get_client(profile)
|
417
|
+
|
418
|
+
result = await client.create_entity(
|
419
|
+
arguments["entityName"], arguments["data"]
|
420
|
+
)
|
421
|
+
|
422
|
+
response = {
|
423
|
+
"success": True,
|
424
|
+
"recordId": result.get("id") if result else None,
|
425
|
+
"createdRecord": (
|
426
|
+
result if arguments.get("returnRecord", False) else None
|
427
|
+
),
|
428
|
+
"validationErrors": None,
|
429
|
+
}
|
430
|
+
|
431
|
+
return [TextContent(type="text", text=json.dumps(response, indent=2))]
|
432
|
+
|
433
|
+
except Exception as e:
|
434
|
+
logger.error(f"Create entity record failed: {e}")
|
435
|
+
error_response = {
|
436
|
+
"success": False,
|
437
|
+
"error": str(e),
|
438
|
+
"tool": "d365fo_create_entity_record",
|
439
|
+
"arguments": arguments,
|
440
|
+
}
|
441
|
+
return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
|
442
|
+
|
443
|
+
async def execute_update_entity_record(self, arguments: dict) -> List[TextContent]:
|
444
|
+
"""Execute update entity record tool.
|
445
|
+
|
446
|
+
Args:
|
447
|
+
arguments: Tool arguments
|
448
|
+
|
449
|
+
Returns:
|
450
|
+
List of TextContent responses
|
451
|
+
"""
|
452
|
+
try:
|
453
|
+
profile = arguments.get("profile", "default")
|
454
|
+
client = await self.client_manager.get_client(profile)
|
455
|
+
|
456
|
+
result = await client.update_entity(
|
457
|
+
arguments["entityName"], arguments["key"], arguments["data"]
|
458
|
+
)
|
459
|
+
|
460
|
+
response = {
|
461
|
+
"success": True,
|
462
|
+
"updatedRecord": (
|
463
|
+
result if arguments.get("returnRecord", False) else None
|
464
|
+
),
|
465
|
+
"validationErrors": None,
|
466
|
+
"conflictDetected": False,
|
467
|
+
}
|
468
|
+
|
469
|
+
return [TextContent(type="text", text=json.dumps(response, indent=2))]
|
470
|
+
|
471
|
+
except Exception as e:
|
472
|
+
logger.error(f"Update entity record failed: {e}")
|
473
|
+
error_response = {
|
474
|
+
"success": False,
|
475
|
+
"error": str(e),
|
476
|
+
"tool": "d365fo_update_entity_record",
|
477
|
+
"arguments": arguments,
|
478
|
+
}
|
479
|
+
return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
|
480
|
+
|
481
|
+
async def execute_delete_entity_record(self, arguments: dict) -> List[TextContent]:
|
482
|
+
"""Execute delete entity record tool.
|
483
|
+
|
484
|
+
Args:
|
485
|
+
arguments: Tool arguments
|
486
|
+
|
487
|
+
Returns:
|
488
|
+
List of TextContent responses
|
489
|
+
"""
|
490
|
+
try:
|
491
|
+
profile = arguments.get("profile", "default")
|
492
|
+
client = await self.client_manager.get_client(profile)
|
493
|
+
|
494
|
+
await client.delete_entity(arguments["entityName"], arguments["key"])
|
495
|
+
|
496
|
+
response = {"success": True, "conflictDetected": False, "error": None}
|
497
|
+
|
498
|
+
return [TextContent(type="text", text=json.dumps(response, indent=2))]
|
499
|
+
|
500
|
+
except Exception as e:
|
501
|
+
logger.error(f"Delete entity record failed: {e}")
|
502
|
+
error_response = {
|
503
|
+
"success": False,
|
504
|
+
"error": str(e),
|
505
|
+
"tool": "d365fo_delete_entity_record",
|
506
|
+
"arguments": arguments,
|
507
|
+
}
|
508
|
+
return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
|
509
|
+
|
510
|
+
async def execute_call_action(self, arguments: dict) -> List[TextContent]:
|
511
|
+
"""Execute call action tool.
|
512
|
+
|
513
|
+
Args:
|
514
|
+
arguments: Tool arguments
|
515
|
+
|
516
|
+
Returns:
|
517
|
+
List of TextContent responses
|
518
|
+
"""
|
519
|
+
try:
|
520
|
+
profile = arguments.get("profile", "default")
|
521
|
+
client = await self.client_manager.get_client(profile)
|
522
|
+
|
523
|
+
action_name = arguments["actionName"]
|
524
|
+
parameters = arguments.get("parameters", {})
|
525
|
+
entity_name = arguments.get("entityName")
|
526
|
+
entity_key = arguments.get("entityKey")
|
527
|
+
binding_kind = arguments.get("bindingKind")
|
528
|
+
|
529
|
+
# Log the action call attempt
|
530
|
+
logger.info(f"Calling action: {action_name}")
|
531
|
+
if entity_name:
|
532
|
+
logger.info(f" Entity: {entity_name}")
|
533
|
+
if entity_key:
|
534
|
+
logger.info(f" Key: {entity_key}")
|
535
|
+
if binding_kind:
|
536
|
+
logger.info(f" Binding: {binding_kind}")
|
537
|
+
|
538
|
+
start_time = time.time()
|
539
|
+
|
540
|
+
# Call the action using the client's call_action method
|
541
|
+
result = await client.call_action(
|
542
|
+
action_name=action_name,
|
543
|
+
parameters=parameters,
|
544
|
+
entity_name=entity_name,
|
545
|
+
entity_key=entity_key,
|
546
|
+
)
|
547
|
+
|
548
|
+
execution_time = time.time() - start_time
|
549
|
+
|
550
|
+
# Format response
|
551
|
+
response = {
|
552
|
+
"success": True,
|
553
|
+
"actionName": action_name,
|
554
|
+
"result": result,
|
555
|
+
"executionTime": round(execution_time, 3),
|
556
|
+
"parameters": parameters,
|
557
|
+
"binding": (
|
558
|
+
{
|
559
|
+
"entityName": entity_name,
|
560
|
+
"entityKey": entity_key,
|
561
|
+
"bindingKind": binding_kind,
|
562
|
+
}
|
563
|
+
if entity_name or entity_key
|
564
|
+
else None
|
565
|
+
),
|
566
|
+
}
|
567
|
+
|
568
|
+
return [TextContent(type="text", text=json.dumps(response, indent=2))]
|
569
|
+
|
570
|
+
except Exception as e:
|
571
|
+
logger.error(f"Call action failed: {e}")
|
572
|
+
error_response = {
|
573
|
+
"success": False,
|
574
|
+
"error": str(e),
|
575
|
+
"tool": "d365fo_call_action",
|
576
|
+
"actionName": arguments.get("actionName"),
|
577
|
+
"arguments": arguments,
|
578
|
+
}
|
579
|
+
return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
|