d365fo-client 0.3.1__py3-none-any.whl → 0.3.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.
d365fo_client/crud.py CHANGED
@@ -1,13 +1,14 @@
1
1
  """CRUD operations for D365 F&O client."""
2
2
 
3
- from typing import Any, Dict, List, Optional, Union
4
-
5
- import aiohttp
3
+ from typing import Any, Dict, Optional, Union, TYPE_CHECKING
6
4
 
7
5
  from .models import QueryOptions
8
6
  from .query import QueryBuilder
9
7
  from .session import SessionManager
10
8
 
9
+ if TYPE_CHECKING:
10
+ from .models import PublicEntityInfo
11
+
11
12
 
12
13
  class CrudOperations:
13
14
  """Handles CRUD operations for F&O entities"""
@@ -23,13 +24,17 @@ class CrudOperations:
23
24
  self.base_url = base_url
24
25
 
25
26
  async def get_entities(
26
- self, entity_name: str, options: Optional[QueryOptions] = None
27
+ self,
28
+ entity_name: str,
29
+ options: Optional[QueryOptions] = None,
30
+ entity_schema: Optional["PublicEntityInfo"] = None
27
31
  ) -> Dict[str, Any]:
28
32
  """Get entities with OData query options
29
33
 
30
34
  Args:
31
35
  entity_name: Name of the entity set
32
36
  options: OData query options
37
+ entity_schema: Optional entity schema for validation/optimization
33
38
 
34
39
  Returns:
35
40
  Response containing entities
@@ -52,6 +57,7 @@ class CrudOperations:
52
57
  entity_name: str,
53
58
  key: Union[str, Dict[str, Any]],
54
59
  options: Optional[QueryOptions] = None,
60
+ entity_schema: Optional["PublicEntityInfo"] = None
55
61
  ) -> Dict[str, Any]:
56
62
  """Get single entity by key
57
63
 
@@ -59,13 +65,15 @@ class CrudOperations:
59
65
  entity_name: Name of the entity set
60
66
  key: Entity key value (string for simple keys, dict for composite keys)
61
67
  options: OData query options
68
+ entity_schema: Optional entity schema for type-aware key encoding
62
69
 
63
70
  Returns:
64
71
  Entity data
65
72
  """
66
73
  session = await self.session_manager.get_session()
67
74
  query_string = QueryBuilder.build_query_string(options)
68
- url = QueryBuilder.build_entity_url(self.base_url, entity_name, key)
75
+ # Use schema-aware URL building for proper key encoding
76
+ url = QueryBuilder.build_entity_url(self.base_url, entity_name, key, entity_schema)
69
77
  url += query_string
70
78
 
71
79
  async with session.get(url) as response:
@@ -78,13 +86,17 @@ class CrudOperations:
78
86
  )
79
87
 
80
88
  async def create_entity(
81
- self, entity_name: str, data: Dict[str, Any]
89
+ self,
90
+ entity_name: str,
91
+ data: Dict[str, Any],
92
+ entity_schema: Optional["PublicEntityInfo"] = None
82
93
  ) -> Dict[str, Any]:
83
94
  """Create new entity
84
95
 
85
96
  Args:
86
97
  entity_name: Name of the entity set
87
98
  data: Entity data to create
99
+ entity_schema: Optional entity schema for validation
88
100
 
89
101
  Returns:
90
102
  Created entity data
@@ -107,6 +119,7 @@ class CrudOperations:
107
119
  key: Union[str, Dict[str, Any]],
108
120
  data: Dict[str, Any],
109
121
  method: str = "PATCH",
122
+ entity_schema: Optional["PublicEntityInfo"] = None
110
123
  ) -> Dict[str, Any]:
111
124
  """Update existing entity
112
125
 
@@ -115,12 +128,14 @@ class CrudOperations:
115
128
  key: Entity key value (string for simple keys, dict for composite keys)
116
129
  data: Updated entity data
117
130
  method: HTTP method (PATCH or PUT)
131
+ entity_schema: Optional entity schema for type-aware key encoding and validation
118
132
 
119
133
  Returns:
120
134
  Updated entity data
121
135
  """
122
136
  session = await self.session_manager.get_session()
123
- url = QueryBuilder.build_entity_url(self.base_url, entity_name, key)
137
+ # Use schema-aware URL building for proper key encoding
138
+ url = QueryBuilder.build_entity_url(self.base_url, entity_name, key, entity_schema)
124
139
 
125
140
  async with session.request(method, url, json=data) as response:
126
141
  if response.status in [200, 204]:
@@ -134,19 +149,24 @@ class CrudOperations:
134
149
  )
135
150
 
136
151
  async def delete_entity(
137
- self, entity_name: str, key: Union[str, Dict[str, Any]]
152
+ self,
153
+ entity_name: str,
154
+ key: Union[str, Dict[str, Any]],
155
+ entity_schema: Optional["PublicEntityInfo"] = None
138
156
  ) -> bool:
139
157
  """Delete entity
140
158
 
141
159
  Args:
142
160
  entity_name: Name of the entity set
143
161
  key: Entity key value (string for simple keys, dict for composite keys)
162
+ entity_schema: Optional entity schema for type-aware key encoding
144
163
 
145
164
  Returns:
146
165
  True if successful
147
166
  """
148
167
  session = await self.session_manager.get_session()
149
- url = QueryBuilder.build_entity_url(self.base_url, entity_name, key)
168
+ # Use schema-aware URL building for proper key encoding
169
+ url = QueryBuilder.build_entity_url(self.base_url, entity_name, key, entity_schema)
150
170
 
151
171
  async with session.delete(url) as response:
152
172
  if response.status in [200, 204]:
@@ -163,6 +183,7 @@ class CrudOperations:
163
183
  parameters: Optional[Dict[str, Any]] = None,
164
184
  entity_name: Optional[str] = None,
165
185
  entity_key: Optional[Union[str, Dict[str, Any]]] = None,
186
+ entity_schema: Optional["PublicEntityInfo"] = None
166
187
  ) -> Any:
167
188
  """Call OData action method
168
189
 
@@ -171,13 +192,15 @@ class CrudOperations:
171
192
  parameters: Action parameters
172
193
  entity_name: Entity name for bound actions
173
194
  entity_key: Entity key for bound actions (string for simple keys, dict for composite keys)
195
+ entity_schema: Optional entity schema for type-aware key encoding in bound actions
174
196
 
175
197
  Returns:
176
198
  Action result
177
199
  """
178
200
  session = await self.session_manager.get_session()
201
+ # Use schema-aware URL building for entity-bound actions
179
202
  url = QueryBuilder.build_action_url(
180
- self.base_url, action_name, entity_name, entity_key
203
+ self.base_url, action_name, entity_name, entity_key, entity_schema
181
204
  )
182
205
 
183
206
  # Prepare request body
d365fo_client/main.py CHANGED
@@ -236,6 +236,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
236
236
  _add_metadata_commands(subparsers)
237
237
  _add_entity_commands(subparsers)
238
238
  _add_action_commands(subparsers)
239
+ _add_service_commands(subparsers)
239
240
  _add_config_commands(subparsers)
240
241
 
241
242
  return parser
@@ -449,6 +450,63 @@ def _add_config_commands(subparsers) -> None:
449
450
  default_parser.add_argument("profile_name", help="Profile name")
450
451
 
451
452
 
453
+ def _add_service_commands(subparsers) -> None:
454
+ """Add JSON service commands."""
455
+ service_parser = subparsers.add_parser("service", help="JSON service operations")
456
+ service_subs = service_parser.add_subparsers(
457
+ dest="service_subcommand", help="Service subcommands"
458
+ )
459
+
460
+ # call subcommand - generic service call
461
+ call_parser = service_subs.add_parser(
462
+ "call", help="Call a generic JSON service endpoint"
463
+ )
464
+ call_parser.add_argument(
465
+ "service_group", help="Service group name (e.g., 'SysSqlDiagnosticService')"
466
+ )
467
+ call_parser.add_argument(
468
+ "service_name",
469
+ help="Service name (e.g., 'SysSqlDiagnosticServiceOperations')"
470
+ )
471
+ call_parser.add_argument(
472
+ "operation_name", help="Operation name (e.g., 'GetAxSqlExecuting')"
473
+ )
474
+ call_parser.add_argument(
475
+ "--parameters",
476
+ help="JSON string with parameters to send in POST body",
477
+ )
478
+
479
+ # sql-diagnostic subcommand - convenience wrapper for SQL diagnostic operations
480
+ sql_parser = service_subs.add_parser(
481
+ "sql-diagnostic", help="Call SQL diagnostic service operations"
482
+ )
483
+ sql_parser.add_argument(
484
+ "operation",
485
+ choices=[
486
+ "GetAxSqlExecuting",
487
+ "GetAxSqlResourceStats",
488
+ "GetAxSqlBlocking",
489
+ "GetAxSqlLockInfo",
490
+ "GetAxSqlDisabledIndexes",
491
+ ],
492
+ help="SQL diagnostic operation to execute",
493
+ )
494
+ sql_parser.add_argument(
495
+ "--since-minutes",
496
+ type=int,
497
+ default=10,
498
+ help="For GetAxSqlResourceStats: get stats for last N minutes (default: 10)",
499
+ )
500
+ sql_parser.add_argument(
501
+ "--start-time",
502
+ help="For GetAxSqlResourceStats: start time (ISO format)",
503
+ )
504
+ sql_parser.add_argument(
505
+ "--end-time",
506
+ help="For GetAxSqlResourceStats: end time (ISO format)",
507
+ )
508
+
509
+
452
510
  def main() -> None:
453
511
  """Enhanced main entry point with CLI support."""
454
512
  parser = create_argument_parser()
@@ -341,7 +341,7 @@ if is_remote_transport:
341
341
  # Initialize FastMCP server with configuration
342
342
  mcp = FastMCP(
343
343
  name=server_config.get("name", "d365fo-mcp-server"),
344
- auth_server_provider=auth_provider if isinstance(auth_provider, AzureProvider) else None,
344
+ auth_server_provider=auth_provider if isinstance(auth_provider, AzureProvider) else None,# type: ignore
345
345
  token_verifier=auth_provider if isinstance(auth_provider, APIKeyVerifier) else None,
346
346
  auth=auth,
347
347
  instructions=server_config.get(
@@ -144,6 +144,10 @@ def create_default_profile_if_needed(profile_manager:"ProfileManager", config:Di
144
144
  if not base_url:
145
145
  logger.warning("Cannot create default profile - D365FO_BASE_URL not set")
146
146
  return False
147
+
148
+ if base_url.startswith("https://usnconeboxax1aos.cloud.onebox.dynamics.com"):
149
+ logger.warning("D365FO_BASE_URL is set to the default onebox URL - please set it to your actual environment URL")
150
+ return False
147
151
 
148
152
  # Determine authentication mode based on startup mode
149
153
  startup_mode = config.get("startup_mode", "profile_only")
@@ -1,7 +1,6 @@
1
1
  """Base mixin class for FastMCP tool categories."""
2
2
 
3
3
  import logging
4
- from typing import Optional
5
4
 
6
5
  from d365fo_client.client import FOClient
7
6
  from d365fo_client.profile_manager import ProfileManager
@@ -13,30 +12,32 @@ logger = logging.getLogger(__name__)
13
12
 
14
13
  class BaseToolsMixin:
15
14
  """Base mixin for FastMCP tool categories.
16
-
15
+
17
16
  Provides common functionality and client access patterns
18
17
  for all tool category mixins.
19
18
  """
20
-
19
+
21
20
  # These will be injected by the main server class
22
21
  client_manager: D365FOClientManager
23
- mcp: FastMCP
24
- profile_manager: ProfileManager
25
-
22
+ mcp: FastMCP
23
+ profile_manager: ProfileManager
24
+
26
25
  async def _get_client(self, profile: str = "default") -> FOClient:
27
26
  """Get D365FO client for specified profile.
28
-
27
+
29
28
  Args:
30
29
  profile: Profile name to use
31
-
30
+
32
31
  Returns:
33
32
  Configured D365FO client instance
34
33
  """
35
- if not hasattr(self, 'client_manager') or not self.client_manager:
34
+ if not hasattr(self, "client_manager") or not self.client_manager:
36
35
  raise RuntimeError("Client manager not initialized")
37
36
  return await self.client_manager.get_client(profile)
38
-
39
- def _create_error_response(self, error: Exception, tool_name: str, arguments: dict) -> dict:
37
+
38
+ def _create_error_response(
39
+ self, error: Exception, tool_name: str, arguments: dict
40
+ ) -> dict:
40
41
  """Create standardized error response.
41
42
 
42
43
  Args:
@@ -52,4 +53,4 @@ class BaseToolsMixin:
52
53
  "tool": tool_name,
53
54
  "arguments": arguments,
54
55
  "error_type": type(error).__name__,
55
- }
56
+ }
@@ -10,10 +10,10 @@ logger = logging.getLogger(__name__)
10
10
 
11
11
  class CrudToolsMixin(BaseToolsMixin):
12
12
  """CRUD (Create, Read, Update, Delete) tools for FastMCP server."""
13
-
13
+
14
14
  def register_crud_tools(self):
15
15
  """Register all CRUD tools with FastMCP."""
16
-
16
+
17
17
  @self.mcp.tool()
18
18
  async def d365fo_query_entities(
19
19
  entity_name: str,
@@ -67,7 +67,7 @@ class CrudToolsMixin(BaseToolsMixin):
67
67
  expand=expand,
68
68
  )
69
69
 
70
- # Execute query
70
+ # FOClient now handles validation and schema fetching
71
71
  result = await client.get_entities(entity_name, options=options)
72
72
 
73
73
  return {
@@ -109,6 +109,18 @@ class CrudToolsMixin(BaseToolsMixin):
109
109
  Dictionary with the entity record
110
110
  """
111
111
  try:
112
+ # Validate key_fields and key_values match
113
+ if len(key_fields) != len(key_values):
114
+ return {
115
+ "error": "Key fields and values length mismatch",
116
+ "entityName": entity_name,
117
+ "key_fields": key_fields,
118
+ "key_values": key_values,
119
+ }
120
+
121
+ # Build key dict
122
+ key = {k: v for k, v in zip(key_fields, key_values)}
123
+
112
124
  client = await self._get_client(profile)
113
125
 
114
126
  # Build query options
@@ -120,19 +132,32 @@ class CrudToolsMixin(BaseToolsMixin):
120
132
  else None
121
133
  )
122
134
 
123
- # Construct key field=value mapping
124
- if len(key_fields) != len(key_values):
125
- raise ValueError("Key fields and values length mismatch")
126
- key = {k: v for k, v in zip(key_fields, key_values)}
127
-
128
- # Get entity record
129
- result = await client.get_entity_by_key(entity_name, key, options) # type: ignore
135
+ # FOClient now handles:
136
+ # - Schema lookup via get_public_entity_schema_by_entityset()
137
+ # - Entity validation (raises FOClientError if not found)
138
+ # - Schema-aware key encoding via QueryBuilder
139
+ result = await client.get_entity(entity_name, key, options)
130
140
 
131
141
  return {"entityName": entity_name, "key": key, "data": result}
132
142
 
133
143
  except Exception as e:
134
144
  logger.error(f"Get entity record failed: {e}")
135
- return {"error": str(e), "entityName": entity_name, "key_fields": key_fields, "key_values": key_values, "key": key}
145
+ # Build key dict for error response if possible
146
+ try:
147
+ key = (
148
+ {k: v for k, v in zip(key_fields, key_values)}
149
+ if len(key_fields) == len(key_values)
150
+ else None
151
+ )
152
+ except Exception:
153
+ key = None
154
+ return {
155
+ "error": str(e),
156
+ "entityName": entity_name,
157
+ "key_fields": key_fields,
158
+ "key_values": key_values,
159
+ "key": key,
160
+ }
136
161
 
137
162
  @self.mcp.tool()
138
163
  async def d365fo_create_entity_record(
@@ -155,10 +180,12 @@ class CrudToolsMixin(BaseToolsMixin):
155
180
  try:
156
181
  client = await self._get_client(profile)
157
182
 
158
- # Create entity record
159
- result = await client.create_entity(
160
- entity_name, data
161
- )
183
+ # FOClient now handles:
184
+ # - Schema validation via get_public_entity_schema_by_entityset()
185
+ # - Entity existence check (raises FOClientError if not found)
186
+ # - Read-only validation (raises FOClientError if read-only)
187
+ # - OData serialization via crud_ops
188
+ result = await client.create_entity(entity_name, data)
162
189
 
163
190
  return {
164
191
  "entityName": entity_name,
@@ -193,18 +220,27 @@ class CrudToolsMixin(BaseToolsMixin):
193
220
  Dictionary with update result
194
221
  """
195
222
  try:
196
- client = await self._get_client(profile)
197
-
198
- # Construct key field=value mapping
223
+ # Validate key_fields and key_values match
199
224
  if len(key_fields) != len(key_values):
200
- raise ValueError("Key fields and values length mismatch")
201
-
225
+ return {
226
+ "error": "Key fields and values length mismatch",
227
+ "entityName": entity_name,
228
+ "key_fields": key_fields,
229
+ "key_values": key_values,
230
+ "updated": False,
231
+ }
232
+
233
+ # Build key dict
202
234
  key = {k: v for k, v in zip(key_fields, key_values)}
203
235
 
204
- # Update entity record
205
- result = await client.update_entity(
206
- entity_name, key, data
207
- )
236
+ client = await self._get_client(profile)
237
+
238
+ # FOClient now handles:
239
+ # - Schema validation via get_public_entity_schema_by_entityset()
240
+ # - Entity existence check (raises FOClientError if not found)
241
+ # - Read-only validation (raises FOClientError if read-only)
242
+ # - Schema-aware key encoding via QueryBuilder
243
+ result = await client.update_entity(entity_name, key, data)
208
244
 
209
245
  return {
210
246
  "entityName": entity_name,
@@ -215,10 +251,19 @@ class CrudToolsMixin(BaseToolsMixin):
215
251
 
216
252
  except Exception as e:
217
253
  logger.error(f"Update entity record failed: {e}")
254
+ # Build key dict for error response if possible
255
+ try:
256
+ key = (
257
+ {k: v for k, v in zip(key_fields, key_values)}
258
+ if len(key_fields) == len(key_values)
259
+ else None
260
+ )
261
+ except Exception:
262
+ key = None
218
263
  return {
219
264
  "error": str(e),
220
265
  "entityName": entity_name,
221
- "key": key, # type: ignore
266
+ "key": key,
222
267
  "updated": False,
223
268
  }
224
269
 
@@ -227,7 +272,7 @@ class CrudToolsMixin(BaseToolsMixin):
227
272
  entity_name: str,
228
273
  key_fields: List[str],
229
274
  key_values: List[str],
230
- profile: str = "default"
275
+ profile: str = "default",
231
276
  ) -> dict:
232
277
  """Delete a record from a D365 Finance & Operations data entity.
233
278
 
@@ -241,35 +286,55 @@ class CrudToolsMixin(BaseToolsMixin):
241
286
  Dictionary with deletion result
242
287
  """
243
288
  try:
244
- client = await self._get_client(profile)
245
-
246
- # Construct key field=value mapping
289
+ # Validate key_fields and key_values match
247
290
  if len(key_fields) != len(key_values):
248
- raise ValueError("Key fields and values length mismatch")
249
-
291
+ return {
292
+ "error": "Key fields and values length mismatch",
293
+ "entityName": entity_name,
294
+ "key_fields": key_fields,
295
+ "key_values": key_values,
296
+ "deleted": False,
297
+ }
298
+
299
+ # Build key dict
250
300
  key = {k: v for k, v in zip(key_fields, key_values)}
251
301
 
252
- # Delete entity record
302
+ client = await self._get_client(profile)
303
+
304
+ # FOClient now handles:
305
+ # - Schema validation via get_public_entity_schema_by_entityset()
306
+ # - Entity existence check (raises FOClientError if not found)
307
+ # - Read-only validation (raises FOClientError if read-only)
308
+ # - Schema-aware key encoding via QueryBuilder
253
309
  await client.delete_entity(entity_name, key)
254
310
 
255
311
  return {"entityName": entity_name, "key": key, "deleted": True}
256
312
 
257
313
  except Exception as e:
258
314
  logger.error(f"Delete entity record failed: {e}")
315
+ # Build key dict for error response if possible
316
+ try:
317
+ key = (
318
+ {k: v for k, v in zip(key_fields, key_values)}
319
+ if len(key_fields) == len(key_values)
320
+ else None
321
+ )
322
+ except Exception:
323
+ key = None
259
324
  return {
260
325
  "error": str(e),
261
326
  "entityName": entity_name,
262
- "key": key, # type: ignore
327
+ "key": key,
263
328
  "deleted": False,
264
329
  }
265
330
 
266
331
  @self.mcp.tool()
267
332
  async def d365fo_call_action(
268
333
  action_name: str,
269
- entity_name: str = None,# type: ignore
270
- parameters: dict = None,# type: ignore
271
- key_fields: List[str] = None,# type: ignore
272
- key_values: List[str] = None,# type: ignore
334
+ entity_name: str = None, # type: ignore
335
+ parameters: dict = None, # type: ignore
336
+ key_fields: List[str] = None, # type: ignore
337
+ key_values: List[str] = None, # type: ignore
273
338
  profile: str = "default",
274
339
  ) -> dict:
275
340
  """Execute an OData action method in D365 Finance & Operations.
@@ -297,15 +362,91 @@ class CrudToolsMixin(BaseToolsMixin):
297
362
  key = {k: v for k, v in zip(key_fields, key_values)}
298
363
 
299
364
  result = await client.call_action(
300
- action_name=action_name,# type: ignore
365
+ action_name=action_name, # type: ignore
301
366
  parameters=parameters or {},
302
367
  entity_name=entity_name,
303
368
  entity_key=key,
304
-
305
369
  )
306
370
 
307
371
  return {"actionName": action_name, "success": True, "result": result}
308
372
 
309
373
  except Exception as e:
310
374
  logger.error(f"Call action failed: {e}")
311
- return {"error": str(e), "actionName": action_name, "success": False}
375
+ return {"error": str(e), "actionName": action_name, "success": False}
376
+
377
+ @self.mcp.tool()
378
+ async def d365fo_call_json_service(
379
+ service_group: str,
380
+ service_name: str,
381
+ operation_name: str,
382
+ parameters: Optional[dict] = None,
383
+ profile: str = "default",
384
+ ) -> dict:
385
+ """Call a D365 F&O JSON service endpoint using the /api/services pattern.
386
+
387
+ This provides a generic way to invoke any JSON service operation in D365 F&O.
388
+
389
+ Args:
390
+ service_group: Service group name (e.g., 'SysSqlDiagnosticService')
391
+ service_name: Service name (e.g., 'SysSqlDiagnosticServiceOperations')
392
+ operation_name: Operation name (e.g., 'GetAxSqlExecuting')
393
+ parameters: Optional parameters to send in the POST body
394
+ profile: Configuration profile to use
395
+
396
+ Returns:
397
+ Dictionary with service response data and metadata
398
+
399
+ Example:
400
+ Call a service without parameters:
401
+ {
402
+ "service_group": "SysSqlDiagnosticService",
403
+ "service_name": "SysSqlDiagnosticServiceOperations",
404
+ "operation_name": "GetAxSqlExecuting"
405
+ }
406
+
407
+ Call a service with parameters:
408
+ {
409
+ "service_group": "SysSqlDiagnosticService",
410
+ "service_name": "SysSqlDiagnosticServiceOperations",
411
+ "operation_name": "GetAxSqlResourceStats",
412
+ "parameters": {
413
+ "start": "2023-01-01T00:00:00Z",
414
+ "end": "2023-01-02T00:00:00Z"
415
+ }
416
+ }
417
+ """
418
+ try:
419
+ client = await self._get_client(profile)
420
+
421
+ # Call the JSON service
422
+ response = await client.post_json_service(
423
+ service_group=service_group,
424
+ service_name=service_name,
425
+ operation_name=operation_name,
426
+ parameters=parameters,
427
+ )
428
+
429
+ # Format response
430
+ result = {
431
+ "success": response.success,
432
+ "statusCode": response.status_code,
433
+ "data": response.data,
434
+ "serviceGroup": service_group,
435
+ "serviceName": service_name,
436
+ "operationName": operation_name,
437
+ }
438
+
439
+ if response.error_message:
440
+ result["errorMessage"] = response.error_message
441
+
442
+ return result
443
+
444
+ except Exception as e:
445
+ logger.error(f"JSON service call failed: {e}")
446
+ return {
447
+ "success": False,
448
+ "error": str(e),
449
+ "serviceGroup": service_group,
450
+ "serviceName": service_name,
451
+ "operationName": operation_name,
452
+ }
@@ -25,7 +25,7 @@ from .resources import (
25
25
  MetadataResourceHandler,
26
26
  QueryResourceHandler,
27
27
  )
28
- from .tools import ConnectionTools, CrudTools, DatabaseTools, LabelTools, MetadataTools, ProfileTools, SyncTools
28
+ from .tools import ConnectionTools, CrudTools, DatabaseTools, JsonServiceTools, LabelTools, MetadataTools, ProfileTools, SyncTools
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -59,6 +59,7 @@ class D365FOMCPServer:
59
59
  self.profile_tools = ProfileTools(self.client_manager)
60
60
  self.database_tools = DatabaseTools(self.client_manager)
61
61
  self.sync_tools = SyncTools(self.client_manager)
62
+ self.json_service_tools = JsonServiceTools(self.client_manager)
62
63
 
63
64
  # Tool registry for execution
64
65
  self.tool_registry = {}
@@ -222,6 +223,10 @@ class D365FOMCPServer:
222
223
  sync_tools = self.sync_tools.get_tools()
223
224
  tools.extend(sync_tools)
224
225
 
226
+ # Add JSON service tools
227
+ json_service_tools = self.json_service_tools.get_tools()
228
+ tools.extend(json_service_tools)
229
+
225
230
  # Register tools for execution
226
231
  for tool in tools:
227
232
  self.tool_registry[tool.name] = tool
@@ -327,6 +332,10 @@ class D365FOMCPServer:
327
332
  return await self.sync_tools.execute_list_sync_sessions(arguments)
328
333
  elif name == "d365fo_get_sync_history":
329
334
  return await self.sync_tools.execute_get_sync_history(arguments)
335
+ elif name == "d365fo_call_json_service":
336
+ return await self.json_service_tools.execute_call_json_service(arguments)
337
+ elif name == "d365fo_call_sql_diagnostic_service":
338
+ return await self.json_service_tools.execute_call_sql_diagnostic_service(arguments)
330
339
  else:
331
340
  raise ValueError(f"Unknown tool: {name}")
332
341