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.
Files changed (51) hide show
  1. d365fo_client/__init__.py +305 -0
  2. d365fo_client/auth.py +93 -0
  3. d365fo_client/cli.py +700 -0
  4. d365fo_client/client.py +1454 -0
  5. d365fo_client/config.py +304 -0
  6. d365fo_client/crud.py +200 -0
  7. d365fo_client/exceptions.py +49 -0
  8. d365fo_client/labels.py +528 -0
  9. d365fo_client/main.py +502 -0
  10. d365fo_client/mcp/__init__.py +16 -0
  11. d365fo_client/mcp/client_manager.py +276 -0
  12. d365fo_client/mcp/main.py +98 -0
  13. d365fo_client/mcp/models.py +371 -0
  14. d365fo_client/mcp/prompts/__init__.py +43 -0
  15. d365fo_client/mcp/prompts/action_execution.py +480 -0
  16. d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
  17. d365fo_client/mcp/resources/__init__.py +15 -0
  18. d365fo_client/mcp/resources/database_handler.py +555 -0
  19. d365fo_client/mcp/resources/entity_handler.py +176 -0
  20. d365fo_client/mcp/resources/environment_handler.py +132 -0
  21. d365fo_client/mcp/resources/metadata_handler.py +283 -0
  22. d365fo_client/mcp/resources/query_handler.py +135 -0
  23. d365fo_client/mcp/server.py +432 -0
  24. d365fo_client/mcp/tools/__init__.py +17 -0
  25. d365fo_client/mcp/tools/connection_tools.py +175 -0
  26. d365fo_client/mcp/tools/crud_tools.py +579 -0
  27. d365fo_client/mcp/tools/database_tools.py +813 -0
  28. d365fo_client/mcp/tools/label_tools.py +189 -0
  29. d365fo_client/mcp/tools/metadata_tools.py +766 -0
  30. d365fo_client/mcp/tools/profile_tools.py +706 -0
  31. d365fo_client/metadata_api.py +793 -0
  32. d365fo_client/metadata_v2/__init__.py +59 -0
  33. d365fo_client/metadata_v2/cache_v2.py +1372 -0
  34. d365fo_client/metadata_v2/database_v2.py +585 -0
  35. d365fo_client/metadata_v2/global_version_manager.py +573 -0
  36. d365fo_client/metadata_v2/search_engine_v2.py +423 -0
  37. d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
  38. d365fo_client/metadata_v2/version_detector.py +439 -0
  39. d365fo_client/models.py +862 -0
  40. d365fo_client/output.py +181 -0
  41. d365fo_client/profile_manager.py +342 -0
  42. d365fo_client/profiles.py +178 -0
  43. d365fo_client/query.py +162 -0
  44. d365fo_client/session.py +60 -0
  45. d365fo_client/utils.py +196 -0
  46. d365fo_client-0.1.0.dist-info/METADATA +1084 -0
  47. d365fo_client-0.1.0.dist-info/RECORD +51 -0
  48. d365fo_client-0.1.0.dist-info/WHEEL +5 -0
  49. d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
  50. d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
  51. d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,706 @@
1
+ """Profile management tools for MCP server."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any, Dict, List
6
+
7
+ from mcp import Tool
8
+ from mcp.types import TextContent
9
+
10
+ from ...profile_manager import EnvironmentProfile, ProfileManager
11
+ from ..client_manager import D365FOClientManager
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ProfileTools:
17
+ """Profile management tools for the MCP server."""
18
+
19
+ def __init__(self, client_manager: D365FOClientManager):
20
+ """Initialize profile tools.
21
+
22
+ Args:
23
+ client_manager: D365FO client manager instance
24
+ """
25
+ self.client_manager = client_manager
26
+ self.profile_manager = ProfileManager()
27
+
28
+ def get_tools(self) -> List[Tool]:
29
+ """Get list of profile management tools.
30
+
31
+ Returns:
32
+ List of Tool definitions
33
+ """
34
+ return [
35
+ self._get_list_profiles_tool(),
36
+ self._get_get_profile_tool(),
37
+ self._get_create_profile_tool(),
38
+ self._get_update_profile_tool(),
39
+ self._get_delete_profile_tool(),
40
+ self._get_set_default_profile_tool(),
41
+ self._get_get_default_profile_tool(),
42
+ self._get_validate_profile_tool(),
43
+ self._get_test_profile_connection_tool(),
44
+ ]
45
+
46
+ def _get_list_profiles_tool(self) -> Tool:
47
+ """Get list profiles tool definition."""
48
+ return Tool(
49
+ name="d365fo_list_profiles",
50
+ description="List all available D365FO environment profiles",
51
+ inputSchema={"type": "object", "properties": {}},
52
+ )
53
+
54
+ def _get_get_profile_tool(self) -> Tool:
55
+ """Get profile tool definition."""
56
+ return Tool(
57
+ name="d365fo_get_profile",
58
+ description="Get details of a specific D365FO environment profile",
59
+ inputSchema={
60
+ "type": "object",
61
+ "properties": {
62
+ "profileName": {
63
+ "type": "string",
64
+ "description": "Name of the profile to retrieve",
65
+ }
66
+ },
67
+ "required": ["profileName"],
68
+ },
69
+ )
70
+
71
+ def _get_create_profile_tool(self) -> Tool:
72
+ """Get create profile tool definition."""
73
+ return Tool(
74
+ name="d365fo_create_profile",
75
+ description="Create a new D365FO environment profile",
76
+ inputSchema={
77
+ "type": "object",
78
+ "properties": {
79
+ "name": {"type": "string", "description": "Profile name"},
80
+ "baseUrl": {"type": "string", "description": "D365FO base URL"},
81
+ "authMode": {
82
+ "type": "string",
83
+ "description": "Authentication mode",
84
+ "enum": ["default", "client_credentials"],
85
+ "default": "default",
86
+ },
87
+ "clientId": {
88
+ "type": "string",
89
+ "description": "Azure client ID (for client_credentials auth)",
90
+ },
91
+ "clientSecret": {
92
+ "type": "string",
93
+ "description": "Azure client secret (for client_credentials auth)",
94
+ },
95
+ "tenantId": {
96
+ "type": "string",
97
+ "description": "Azure tenant ID (for client_credentials auth)",
98
+ },
99
+ "verifySsl": {
100
+ "type": "boolean",
101
+ "description": "Whether to verify SSL certificates",
102
+ "default": True,
103
+ },
104
+ "timeout": {
105
+ "type": "integer",
106
+ "description": "Request timeout in seconds",
107
+ "minimum": 1,
108
+ "maximum": 300,
109
+ "default": 60,
110
+ },
111
+ "useLabelCache": {
112
+ "type": "boolean",
113
+ "description": "Whether to enable label caching",
114
+ "default": True,
115
+ },
116
+ "labelCacheExpiryMinutes": {
117
+ "type": "integer",
118
+ "description": "Label cache expiry in minutes",
119
+ "minimum": 1,
120
+ "default": 60,
121
+ },
122
+ "language": {
123
+ "type": "string",
124
+ "description": "Default language code",
125
+ "default": "en-US",
126
+ },
127
+ "cacheDir": {
128
+ "type": "string",
129
+ "description": "Cache directory path",
130
+ },
131
+ "description": {
132
+ "type": "string",
133
+ "description": "Profile description",
134
+ },
135
+ "setAsDefault": {
136
+ "type": "boolean",
137
+ "description": "Set as default profile",
138
+ "default": False,
139
+ },
140
+ },
141
+ "required": ["name", "baseUrl"],
142
+ },
143
+ )
144
+
145
+ def _get_update_profile_tool(self) -> Tool:
146
+ """Get update profile tool definition."""
147
+ return Tool(
148
+ name="d365fo_update_profile",
149
+ description="Update an existing D365FO environment profile",
150
+ inputSchema={
151
+ "type": "object",
152
+ "properties": {
153
+ "name": {"type": "string", "description": "Profile name"},
154
+ "baseUrl": {"type": "string", "description": "D365FO base URL"},
155
+ "authMode": {
156
+ "type": "string",
157
+ "description": "Authentication mode",
158
+ "enum": ["default", "client_credentials"],
159
+ },
160
+ "clientId": {"type": "string", "description": "Azure client ID"},
161
+ "clientSecret": {
162
+ "type": "string",
163
+ "description": "Azure client secret",
164
+ },
165
+ "tenantId": {"type": "string", "description": "Azure tenant ID"},
166
+ "verifySsl": {
167
+ "type": "boolean",
168
+ "description": "Whether to verify SSL certificates",
169
+ },
170
+ "timeout": {
171
+ "type": "integer",
172
+ "description": "Request timeout in seconds",
173
+ "minimum": 1,
174
+ "maximum": 300,
175
+ },
176
+ "useLabelCache": {
177
+ "type": "boolean",
178
+ "description": "Whether to enable label caching",
179
+ },
180
+ "labelCacheExpiryMinutes": {
181
+ "type": "integer",
182
+ "description": "Label cache expiry in minutes",
183
+ "minimum": 1,
184
+ },
185
+ "language": {
186
+ "type": "string",
187
+ "description": "Default language code",
188
+ },
189
+ "cacheDir": {
190
+ "type": "string",
191
+ "description": "Cache directory path",
192
+ },
193
+ "description": {
194
+ "type": "string",
195
+ "description": "Profile description",
196
+ },
197
+ },
198
+ "required": ["name"],
199
+ },
200
+ )
201
+
202
+ def _get_delete_profile_tool(self) -> Tool:
203
+ """Get delete profile tool definition."""
204
+ return Tool(
205
+ name="d365fo_delete_profile",
206
+ description="Delete a D365FO environment profile",
207
+ inputSchema={
208
+ "type": "object",
209
+ "properties": {
210
+ "profileName": {
211
+ "type": "string",
212
+ "description": "Name of the profile to delete",
213
+ }
214
+ },
215
+ "required": ["profileName"],
216
+ },
217
+ )
218
+
219
+ def _get_set_default_profile_tool(self) -> Tool:
220
+ """Get set default profile tool definition."""
221
+ return Tool(
222
+ name="d365fo_set_default_profile",
223
+ description="Set the default D365FO environment profile",
224
+ inputSchema={
225
+ "type": "object",
226
+ "properties": {
227
+ "profileName": {
228
+ "type": "string",
229
+ "description": "Name of the profile to set as default",
230
+ }
231
+ },
232
+ "required": ["profileName"],
233
+ },
234
+ )
235
+
236
+ def _get_get_default_profile_tool(self) -> Tool:
237
+ """Get default profile tool definition."""
238
+ return Tool(
239
+ name="d365fo_get_default_profile",
240
+ description="Get the current default D365FO environment profile",
241
+ inputSchema={"type": "object", "properties": {}},
242
+ )
243
+
244
+ def _get_validate_profile_tool(self) -> Tool:
245
+ """Get validate profile tool definition."""
246
+ return Tool(
247
+ name="d365fo_validate_profile",
248
+ description="Validate a D365FO environment profile configuration",
249
+ inputSchema={
250
+ "type": "object",
251
+ "properties": {
252
+ "profileName": {
253
+ "type": "string",
254
+ "description": "Name of the profile to validate",
255
+ }
256
+ },
257
+ "required": ["profileName"],
258
+ },
259
+ )
260
+
261
+ def _get_test_profile_connection_tool(self) -> Tool:
262
+ """Get test profile connection tool definition."""
263
+ return Tool(
264
+ name="d365fo_test_profile_connection",
265
+ description="Test connection for a specific D365FO environment profile",
266
+ inputSchema={
267
+ "type": "object",
268
+ "properties": {
269
+ "profileName": {
270
+ "type": "string",
271
+ "description": "Name of the profile to test",
272
+ }
273
+ },
274
+ "required": ["profileName"],
275
+ },
276
+ )
277
+
278
+ async def execute_list_profiles(self, arguments: dict) -> List[TextContent]:
279
+ """Execute list profiles tool.
280
+
281
+ Args:
282
+ arguments: Tool arguments
283
+
284
+ Returns:
285
+ List of TextContent responses
286
+ """
287
+ try:
288
+ profiles = self.profile_manager.list_profiles()
289
+ default_profile = self.profile_manager.get_default_profile()
290
+
291
+ profile_list = []
292
+ for name, profile in profiles.items():
293
+ profile_info = {
294
+ "name": profile.name,
295
+ "baseUrl": profile.base_url,
296
+ "authMode": profile.auth_mode,
297
+ "verifySsl": profile.verify_ssl,
298
+ "language": profile.language,
299
+ "isDefault": default_profile and default_profile.name == name,
300
+ "description": profile.description,
301
+ }
302
+ profile_list.append(profile_info)
303
+
304
+ response = {
305
+ "profiles": profile_list,
306
+ "totalCount": len(profile_list),
307
+ "defaultProfile": default_profile.name if default_profile else None,
308
+ }
309
+
310
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
311
+
312
+ except Exception as e:
313
+ logger.error(f"List profiles failed: {e}")
314
+ error_response = {"error": str(e), "tool": "d365fo_list_profiles"}
315
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
316
+
317
+ async def execute_get_profile(self, arguments: dict) -> List[TextContent]:
318
+ """Execute get profile tool.
319
+
320
+ Args:
321
+ arguments: Tool arguments
322
+
323
+ Returns:
324
+ List of TextContent responses
325
+ """
326
+ try:
327
+ profile_name = arguments["profileName"]
328
+ profile = self.profile_manager.get_profile(profile_name)
329
+
330
+ if not profile:
331
+ error_response = {
332
+ "error": f"Profile not found: {profile_name}",
333
+ "tool": "d365fo_get_profile",
334
+ }
335
+ return [
336
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
337
+ ]
338
+
339
+ # Convert profile to dict, excluding sensitive data
340
+ profile_dict = {
341
+ "name": profile.name,
342
+ "baseUrl": profile.base_url,
343
+ "authMode": profile.auth_mode,
344
+ "verifySsl": profile.verify_ssl,
345
+ "timeout": profile.timeout,
346
+ "useLabelCache": profile.use_label_cache,
347
+ "labelCacheExpiryMinutes": profile.label_cache_expiry_minutes,
348
+ "language": profile.language,
349
+ "cacheDir": profile.cache_dir,
350
+ "description": profile.description,
351
+ }
352
+
353
+ # Add auth details if available (but not secrets)
354
+ if profile.client_id:
355
+ profile_dict["clientId"] = profile.client_id
356
+ if profile.tenant_id:
357
+ profile_dict["tenantId"] = profile.tenant_id
358
+
359
+ return [TextContent(type="text", text=json.dumps(profile_dict, indent=2))]
360
+
361
+ except Exception as e:
362
+ logger.error(f"Get profile failed: {e}")
363
+ error_response = {
364
+ "error": str(e),
365
+ "tool": "d365fo_get_profile",
366
+ "arguments": arguments,
367
+ }
368
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
369
+
370
+ async def execute_create_profile(self, arguments: dict) -> List[TextContent]:
371
+ """Execute create profile tool.
372
+
373
+ Args:
374
+ arguments: Tool arguments
375
+
376
+ Returns:
377
+ List of TextContent responses
378
+ """
379
+ try:
380
+ # Extract parameters
381
+ name = arguments["name"]
382
+ base_url = arguments["baseUrl"]
383
+ auth_mode = arguments.get("authMode", "default")
384
+ client_id = arguments.get("clientId")
385
+ client_secret = arguments.get("clientSecret")
386
+ tenant_id = arguments.get("tenantId")
387
+ verify_ssl = arguments.get("verifySsl", True)
388
+ timeout = arguments.get("timeout", 60)
389
+ use_label_cache = arguments.get("useLabelCache", True)
390
+ label_cache_expiry_minutes = arguments.get("labelCacheExpiryMinutes", 60)
391
+ language = arguments.get("language", "en-US")
392
+ cache_dir = arguments.get("cacheDir")
393
+ description = arguments.get("description")
394
+ set_as_default = arguments.get("setAsDefault", False)
395
+
396
+ # Create profile
397
+ success = self.profile_manager.create_profile(
398
+ name=name,
399
+ base_url=base_url,
400
+ auth_mode=auth_mode,
401
+ client_id=client_id,
402
+ client_secret=client_secret,
403
+ tenant_id=tenant_id,
404
+ verify_ssl=verify_ssl,
405
+ timeout=timeout,
406
+ use_label_cache=use_label_cache,
407
+ label_cache_expiry_minutes=label_cache_expiry_minutes,
408
+ language=language,
409
+ cache_dir=cache_dir,
410
+ description=description,
411
+ )
412
+
413
+ if not success:
414
+ error_response = {
415
+ "error": f"Failed to create profile: {name}",
416
+ "tool": "d365fo_create_profile",
417
+ }
418
+ return [
419
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
420
+ ]
421
+
422
+ # Set as default if requested
423
+ if set_as_default:
424
+ self.profile_manager.set_default_profile(name)
425
+
426
+ response = {
427
+ "success": True,
428
+ "profileName": name,
429
+ "message": f"Profile '{name}' created successfully",
430
+ "isDefault": set_as_default,
431
+ }
432
+
433
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
434
+
435
+ except Exception as e:
436
+ logger.error(f"Create profile failed: {e}")
437
+ error_response = {
438
+ "error": str(e),
439
+ "tool": "d365fo_create_profile",
440
+ "arguments": arguments,
441
+ }
442
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
443
+
444
+ async def execute_update_profile(self, arguments: dict) -> List[TextContent]:
445
+ """Execute update profile tool.
446
+
447
+ Args:
448
+ arguments: Tool arguments
449
+
450
+ Returns:
451
+ List of TextContent responses
452
+ """
453
+ try:
454
+ name = arguments["name"]
455
+
456
+ # Remove name from update parameters
457
+ update_params = {k: v for k, v in arguments.items() if k != "name"}
458
+
459
+ # Convert parameter names to match profile manager
460
+ param_mapping = {
461
+ "baseUrl": "base_url",
462
+ "authMode": "auth_mode",
463
+ "clientId": "client_id",
464
+ "clientSecret": "client_secret",
465
+ "tenantId": "tenant_id",
466
+ "verifySsl": "verify_ssl",
467
+ "useLabelCache": "use_label_cache",
468
+ "labelCacheExpiryMinutes": "label_cache_expiry_minutes",
469
+ "cacheDir": "cache_dir",
470
+ }
471
+
472
+ mapped_params = {}
473
+ for key, value in update_params.items():
474
+ mapped_key = param_mapping.get(key, key)
475
+ mapped_params[mapped_key] = value
476
+
477
+ success = self.profile_manager.update_profile(name, **mapped_params)
478
+
479
+ if not success:
480
+ error_response = {
481
+ "error": f"Failed to update profile: {name}",
482
+ "tool": "d365fo_update_profile",
483
+ }
484
+ return [
485
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
486
+ ]
487
+
488
+ response = {
489
+ "success": True,
490
+ "profileName": name,
491
+ "message": f"Profile '{name}' updated successfully",
492
+ "updatedFields": list(update_params.keys()),
493
+ }
494
+
495
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
496
+
497
+ except Exception as e:
498
+ logger.error(f"Update profile failed: {e}")
499
+ error_response = {
500
+ "error": str(e),
501
+ "tool": "d365fo_update_profile",
502
+ "arguments": arguments,
503
+ }
504
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
505
+
506
+ async def execute_delete_profile(self, arguments: dict) -> List[TextContent]:
507
+ """Execute delete profile tool.
508
+
509
+ Args:
510
+ arguments: Tool arguments
511
+
512
+ Returns:
513
+ List of TextContent responses
514
+ """
515
+ try:
516
+ profile_name = arguments["profileName"]
517
+ success = self.profile_manager.delete_profile(profile_name)
518
+
519
+ if not success:
520
+ error_response = {
521
+ "error": f"Profile not found or failed to delete: {profile_name}",
522
+ "tool": "d365fo_delete_profile",
523
+ }
524
+ return [
525
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
526
+ ]
527
+
528
+ response = {
529
+ "success": True,
530
+ "profileName": profile_name,
531
+ "message": f"Profile '{profile_name}' deleted successfully",
532
+ }
533
+
534
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
535
+
536
+ except Exception as e:
537
+ logger.error(f"Delete profile failed: {e}")
538
+ error_response = {
539
+ "error": str(e),
540
+ "tool": "d365fo_delete_profile",
541
+ "arguments": arguments,
542
+ }
543
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
544
+
545
+ async def execute_set_default_profile(self, arguments: dict) -> List[TextContent]:
546
+ """Execute set default profile tool.
547
+
548
+ Args:
549
+ arguments: Tool arguments
550
+
551
+ Returns:
552
+ List of TextContent responses
553
+ """
554
+ try:
555
+ profile_name = arguments["profileName"]
556
+ success = self.profile_manager.set_default_profile(profile_name)
557
+
558
+ if not success:
559
+ error_response = {
560
+ "error": f"Profile not found: {profile_name}",
561
+ "tool": "d365fo_set_default_profile",
562
+ }
563
+ return [
564
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
565
+ ]
566
+
567
+ response = {
568
+ "success": True,
569
+ "profileName": profile_name,
570
+ "message": f"Default profile set to '{profile_name}'",
571
+ }
572
+
573
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
574
+
575
+ except Exception as e:
576
+ logger.error(f"Set default profile failed: {e}")
577
+ error_response = {
578
+ "error": str(e),
579
+ "tool": "d365fo_set_default_profile",
580
+ "arguments": arguments,
581
+ }
582
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
583
+
584
+ async def execute_get_default_profile(self, arguments: dict) -> List[TextContent]:
585
+ """Execute get default profile tool.
586
+
587
+ Args:
588
+ arguments: Tool arguments
589
+
590
+ Returns:
591
+ List of TextContent responses
592
+ """
593
+ try:
594
+ default_profile = self.profile_manager.get_default_profile()
595
+
596
+ if not default_profile:
597
+ response = {"defaultProfile": None, "message": "No default profile set"}
598
+ else:
599
+ response = {
600
+ "defaultProfile": {
601
+ "name": default_profile.name,
602
+ "baseUrl": default_profile.base_url,
603
+ "authMode": default_profile.auth_mode,
604
+ "description": default_profile.description,
605
+ },
606
+ "message": f"Default profile is '{default_profile.name}'",
607
+ }
608
+
609
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
610
+
611
+ except Exception as e:
612
+ logger.error(f"Get default profile failed: {e}")
613
+ error_response = {"error": str(e), "tool": "d365fo_get_default_profile"}
614
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
615
+
616
+ async def execute_validate_profile(self, arguments: dict) -> List[TextContent]:
617
+ """Execute validate profile tool.
618
+
619
+ Args:
620
+ arguments: Tool arguments
621
+
622
+ Returns:
623
+ List of TextContent responses
624
+ """
625
+ try:
626
+ profile_name = arguments["profileName"]
627
+ profile = self.profile_manager.get_profile(profile_name)
628
+
629
+ if not profile:
630
+ error_response = {
631
+ "error": f"Profile not found: {profile_name}",
632
+ "tool": "d365fo_validate_profile",
633
+ }
634
+ return [
635
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
636
+ ]
637
+
638
+ validation_errors = self.profile_manager.validate_profile(profile)
639
+
640
+ response = {
641
+ "profileName": profile_name,
642
+ "isValid": len(validation_errors) == 0,
643
+ "errors": validation_errors,
644
+ "message": (
645
+ "Profile is valid"
646
+ if not validation_errors
647
+ else "Profile has validation errors"
648
+ ),
649
+ }
650
+
651
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
652
+
653
+ except Exception as e:
654
+ logger.error(f"Validate profile failed: {e}")
655
+ error_response = {
656
+ "error": str(e),
657
+ "tool": "d365fo_validate_profile",
658
+ "arguments": arguments,
659
+ }
660
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]
661
+
662
+ async def execute_test_profile_connection(
663
+ self, arguments: dict
664
+ ) -> List[TextContent]:
665
+ """Execute test profile connection tool.
666
+
667
+ Args:
668
+ arguments: Tool arguments
669
+
670
+ Returns:
671
+ List of TextContent responses
672
+ """
673
+ try:
674
+ profile_name = arguments["profileName"]
675
+ profile = self.profile_manager.get_profile(profile_name)
676
+
677
+ if not profile:
678
+ error_response = {
679
+ "error": f"Profile not found: {profile_name}",
680
+ "tool": "d365fo_test_profile_connection",
681
+ }
682
+ return [
683
+ TextContent(type="text", text=json.dumps(error_response, indent=2))
684
+ ]
685
+
686
+ # Test connection using the client manager
687
+ success = await self.client_manager.test_connection(profile_name)
688
+
689
+ response = {
690
+ "profileName": profile_name,
691
+ "success": success,
692
+ "baseUrl": profile.base_url,
693
+ "authMode": profile.auth_mode,
694
+ "message": "Connection successful" if success else "Connection failed",
695
+ }
696
+
697
+ return [TextContent(type="text", text=json.dumps(response, indent=2))]
698
+
699
+ except Exception as e:
700
+ logger.error(f"Test profile connection failed: {e}")
701
+ error_response = {
702
+ "error": str(e),
703
+ "tool": "d365fo_test_profile_connection",
704
+ "arguments": arguments,
705
+ }
706
+ return [TextContent(type="text", text=json.dumps(error_response, indent=2))]