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,176 @@
1
+ """Entity resource handler for MCP server."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from time import timezone
7
+ from typing import List
8
+
9
+ from mcp.types import Resource
10
+
11
+ from ..client_manager import D365FOClientManager
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class EntityResourceHandler:
17
+ """Handles entity resources for the MCP server."""
18
+
19
+ def __init__(self, client_manager: D365FOClientManager):
20
+ """Initialize the entity resource handler.
21
+
22
+ Args:
23
+ client_manager: D365FO client manager instance
24
+ """
25
+ self.client_manager = client_manager
26
+
27
+ async def list_resources(self) -> List[Resource]:
28
+ """List available entity resources.
29
+
30
+ Returns:
31
+ List of entity resources
32
+ """
33
+ try:
34
+ client = await self.client_manager.get_client()
35
+ entities = await client.search_entities("")
36
+
37
+ resources = []
38
+ for entity_name in entities[:100]: # Limit to first 100 for performance
39
+ resources.append(
40
+ Resource(
41
+ uri=f"d365fo://entities/{entity_name}",
42
+ name=f"Entity: {entity_name}",
43
+ description=f"D365FO entity {entity_name} with metadata and sample data",
44
+ mimeType="application/json",
45
+ )
46
+ )
47
+
48
+ logger.info(f"Listed {len(resources)} entity resources")
49
+ return resources
50
+ except Exception as e:
51
+ logger.error(f"Failed to list entity resources: {e}")
52
+ raise
53
+
54
+ async def read_resource(self, uri: str) -> str:
55
+ """Read specific entity resource.
56
+
57
+ Args:
58
+ uri: Resource URI (e.g., "d365fo://entities/Customers")
59
+
60
+ Returns:
61
+ JSON string with entity resource content
62
+ """
63
+ entity_name = self._extract_entity_name(uri)
64
+ client = await self.client_manager.get_client()
65
+
66
+ try:
67
+ # Get entity metadata
68
+ entity_info = await client.get_entity_info(entity_name)
69
+
70
+ # Get sample data (limited to 5 records)
71
+ from ...models import QueryOptions
72
+
73
+ sample_data = await client.get_entities(
74
+ entity_name, options=QueryOptions(top=5)
75
+ )
76
+
77
+ # Build resource content
78
+ metadata = None
79
+ if entity_info:
80
+ # Handle both object and dictionary return types
81
+ if isinstance(entity_info, dict):
82
+ metadata = {
83
+ "name": entity_info.get("name", entity_name),
84
+ "entitySetName": entity_info.get(
85
+ "entity_set_name", entity_name
86
+ ),
87
+ "keys": entity_info.get("keys", []),
88
+ "properties": [
89
+ {
90
+ "name": (
91
+ prop.get("name", "")
92
+ if isinstance(prop, dict)
93
+ else prop.name
94
+ ),
95
+ "type": (
96
+ prop.get("type", "")
97
+ if isinstance(prop, dict)
98
+ else getattr(prop, "type", "")
99
+ ),
100
+ "isKey": (
101
+ prop.get("name", "")
102
+ if isinstance(prop, dict)
103
+ else prop.name
104
+ )
105
+ in entity_info.get("keys", []),
106
+ "maxLength": (
107
+ prop.get("max_length")
108
+ if isinstance(prop, dict)
109
+ else getattr(prop, "max_length", None)
110
+ ),
111
+ "label": (
112
+ prop.get("label_text")
113
+ if isinstance(prop, dict)
114
+ else getattr(prop, "label_text", None)
115
+ ),
116
+ }
117
+ for prop in entity_info.get("properties", [])
118
+ ],
119
+ "isReadOnly": entity_info.get("is_read_only", False),
120
+ "labelText": entity_info.get("label_text", None),
121
+ }
122
+ else:
123
+ metadata = {
124
+ "name": getattr(entity_info, "name", entity_name),
125
+ "entitySetName": getattr(
126
+ entity_info, "entity_set_name", entity_name
127
+ ),
128
+ "keys": getattr(entity_info, "keys", []),
129
+ "properties": [
130
+ {
131
+ "name": getattr(prop, "name", ""),
132
+ "type": getattr(prop, "type", "")
133
+ or getattr(prop, "type_name", ""),
134
+ "isKey": getattr(prop, "name", "")
135
+ in getattr(entity_info, "keys", []),
136
+ "maxLength": getattr(prop, "max_length", None),
137
+ "label": getattr(prop, "label_text", None),
138
+ }
139
+ for prop in getattr(entity_info, "properties", [])
140
+ ],
141
+ "isReadOnly": getattr(entity_info, "is_read_only", False),
142
+ "labelText": getattr(entity_info, "label_text", None),
143
+ }
144
+
145
+ resource_content = {
146
+ "metadata": metadata,
147
+ "sampleData": sample_data.get("value", []) if sample_data else [],
148
+ "recordCount": sample_data.get("@odata.count") if sample_data else None,
149
+ "lastUpdated": datetime.now(timezone.utc).isoformat(),
150
+ }
151
+
152
+ logger.info(f"Retrieved entity resource: {entity_name}")
153
+ return json.dumps(resource_content, indent=2)
154
+
155
+ except Exception as e:
156
+ logger.error(f"Failed to read entity resource {entity_name}: {e}")
157
+ # Return error in resource format
158
+ error_content = {
159
+ "error": str(e),
160
+ "entityName": entity_name,
161
+ "timestamp": datetime.utcnow().isoformat(),
162
+ }
163
+ return json.dumps(error_content, indent=2)
164
+
165
+ def _extract_entity_name(self, uri: str) -> str:
166
+ """Extract entity name from resource URI.
167
+
168
+ Args:
169
+ uri: Resource URI
170
+
171
+ Returns:
172
+ Entity name
173
+ """
174
+ if uri.startswith("d365fo://entities/"):
175
+ return uri[len("d365fo://entities/") :]
176
+ raise ValueError(f"Invalid entity resource URI: {uri}")
@@ -0,0 +1,132 @@
1
+ """Environment resource handler for MCP server."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from time import timezone
7
+ from typing import List
8
+
9
+ from mcp.types import Resource
10
+
11
+ from ..client_manager import D365FOClientManager
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class EnvironmentResourceHandler:
17
+ """Handles environment resources for the MCP server."""
18
+
19
+ def __init__(self, client_manager: D365FOClientManager):
20
+ """Initialize the environment resource handler.
21
+
22
+ Args:
23
+ client_manager: D365FO client manager instance
24
+ """
25
+ self.client_manager = client_manager
26
+
27
+ async def list_resources(self) -> List[Resource]:
28
+ """List available environment resources.
29
+
30
+ Returns:
31
+ List of environment resources
32
+ """
33
+ resources = [
34
+ Resource(
35
+ uri="d365fo://environment/status",
36
+ name="Environment Status",
37
+ description="Environment health and connectivity status",
38
+ mimeType="application/json",
39
+ ),
40
+ Resource(
41
+ uri="d365fo://environment/version",
42
+ name="Version Information",
43
+ description="D365FO application and platform version information",
44
+ mimeType="application/json",
45
+ ),
46
+ Resource(
47
+ uri="d365fo://environment/cache",
48
+ name="Cache Status",
49
+ description="Cache status and performance statistics",
50
+ mimeType="application/json",
51
+ ),
52
+ ]
53
+
54
+ logger.info(f"Listed {len(resources)} environment resources")
55
+ return resources
56
+
57
+ async def read_resource(self, uri: str) -> str:
58
+ """Read specific environment resource.
59
+
60
+ Args:
61
+ uri: Resource URI
62
+
63
+ Returns:
64
+ JSON string with environment resource content
65
+ """
66
+ try:
67
+ if uri == "d365fo://environment/status":
68
+ return await self._get_status_resource()
69
+ elif uri == "d365fo://environment/version":
70
+ return await self._get_version_resource()
71
+ elif uri == "d365fo://environment/cache":
72
+ return await self._get_cache_resource()
73
+ else:
74
+ raise ValueError(f"Unknown environment resource URI: {uri}")
75
+ except Exception as e:
76
+ logger.error(f"Failed to read environment resource {uri}: {e}")
77
+ error_content = {
78
+ "error": str(e),
79
+ "uri": uri,
80
+ "timestamp": datetime.now(timezone.utc),
81
+ }
82
+ return json.dumps(error_content, indent=2)
83
+
84
+ async def _get_status_resource(self) -> str:
85
+ """Get environment status resource."""
86
+ try:
87
+ env_info = await self.client_manager.get_environment_info()
88
+ health_check = await self.client_manager.health_check()
89
+
90
+ status_content = {
91
+ "baseUrl": env_info["base_url"],
92
+ "connectivity": env_info["connectivity"],
93
+ "healthCheck": health_check,
94
+ "lastUpdated": datetime.now(timezone.utc),
95
+ }
96
+
97
+ return json.dumps(status_content, indent=2)
98
+ except Exception as e:
99
+ logger.error(f"Failed to get status resource: {e}")
100
+ raise
101
+
102
+ async def _get_version_resource(self) -> str:
103
+ """Get version information resource."""
104
+ try:
105
+ env_info = await self.client_manager.get_environment_info()
106
+
107
+ version_content = {
108
+ "baseUrl": env_info["base_url"],
109
+ "versions": env_info["versions"],
110
+ "lastUpdated": datetime.now(timezone.utc),
111
+ }
112
+
113
+ return json.dumps(version_content, indent=2)
114
+ except Exception as e:
115
+ logger.error(f"Failed to get version resource: {e}")
116
+ raise
117
+
118
+ async def _get_cache_resource(self) -> str:
119
+ """Get cache status resource."""
120
+ try:
121
+ env_info = await self.client_manager.get_environment_info()
122
+
123
+ cache_content = {
124
+ "baseUrl": env_info["base_url"],
125
+ "cacheStatus": env_info["cache_status"],
126
+ "lastUpdated": datetime.now(timezone.utc),
127
+ }
128
+
129
+ return json.dumps(cache_content, indent=2)
130
+ except Exception as e:
131
+ logger.error(f"Failed to get cache resource: {e}")
132
+ raise
@@ -0,0 +1,283 @@
1
+ """Metadata resource handler for MCP server."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import List
7
+
8
+ from mcp.types import Resource
9
+
10
+ from ..client_manager import D365FOClientManager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class MetadataResourceHandler:
16
+ """Handles metadata resources for the MCP server."""
17
+
18
+ def __init__(self, client_manager: D365FOClientManager):
19
+ """Initialize the metadata resource handler.
20
+
21
+ Args:
22
+ client_manager: D365FO client manager instance
23
+ """
24
+ self.client_manager = client_manager
25
+
26
+ async def list_resources(self) -> List[Resource]:
27
+ """List available metadata resources.
28
+
29
+ Returns:
30
+ List of metadata resources
31
+ """
32
+ resources = [
33
+ Resource(
34
+ uri="d365fo://metadata/entities",
35
+ name="Data Entities Metadata",
36
+ description="All data entities metadata and schemas",
37
+ mimeType="application/json",
38
+ ),
39
+ Resource(
40
+ uri="d365fo://metadata/actions",
41
+ name="OData Actions",
42
+ description="Available OData actions and functions",
43
+ mimeType="application/json",
44
+ ),
45
+ Resource(
46
+ uri="d365fo://metadata/enumerations",
47
+ name="System Enumerations",
48
+ description="System enumerations and their values",
49
+ mimeType="application/json",
50
+ ),
51
+ Resource(
52
+ uri="d365fo://metadata/labels",
53
+ name="System Labels",
54
+ description="System labels and translations",
55
+ mimeType="application/json",
56
+ ),
57
+ ]
58
+
59
+ logger.info(f"Listed {len(resources)} metadata resources")
60
+ return resources
61
+
62
+ async def read_resource(self, uri: str) -> str:
63
+ """Read specific metadata resource.
64
+
65
+ Args:
66
+ uri: Resource URI
67
+
68
+ Returns:
69
+ JSON string with metadata resource content
70
+ """
71
+ try:
72
+ if uri == "d365fo://metadata/entities":
73
+ return await self._get_entities_metadata()
74
+ elif uri == "d365fo://metadata/actions":
75
+ return await self._get_actions_metadata()
76
+ elif uri == "d365fo://metadata/enumerations":
77
+ return await self._get_enumerations_metadata()
78
+ elif uri == "d365fo://metadata/labels":
79
+ return await self._get_labels_metadata()
80
+ else:
81
+ raise ValueError(f"Unknown metadata resource URI: {uri}")
82
+ except Exception as e:
83
+ logger.error(f"Failed to read metadata resource {uri}: {e}")
84
+ error_content = {
85
+ "error": str(e),
86
+ "uri": uri,
87
+ "timestamp": datetime.utcnow().isoformat(),
88
+ }
89
+ return json.dumps(error_content, indent=2)
90
+
91
+ async def _get_entities_metadata(self) -> str:
92
+ """Get entities metadata resource."""
93
+ try:
94
+ client = await self.client_manager.get_client()
95
+
96
+ # Get all entities
97
+ entities = await client.search_entities("")
98
+
99
+ # Get detailed info for first 50 entities (performance limit)
100
+ detailed_entities = []
101
+ for entity_name in entities[:50]:
102
+ entity_info = await client.get_entity_info(entity_name)
103
+ if entity_info:
104
+ # Handle both object and dictionary return types
105
+ if isinstance(entity_info, dict):
106
+ detailed_entities.append(
107
+ {
108
+ "name": entity_info.get("name", entity_name),
109
+ "entitySetName": entity_info.get("entity_set_name", ""),
110
+ "keys": entity_info.get("keys", []),
111
+ "propertyCount": len(entity_info.get("properties", [])),
112
+ "isReadOnly": entity_info.get("is_read_only", False),
113
+ "labelText": entity_info.get("label_text", ""),
114
+ }
115
+ )
116
+ else:
117
+ detailed_entities.append(
118
+ {
119
+ "name": getattr(entity_info, "name", entity_name),
120
+ "entitySetName": getattr(
121
+ entity_info, "entity_set_name", ""
122
+ ),
123
+ "keys": getattr(entity_info, "keys", []),
124
+ "propertyCount": len(
125
+ getattr(entity_info, "properties", [])
126
+ ),
127
+ "isReadOnly": getattr(
128
+ entity_info, "is_read_only", False
129
+ ),
130
+ "labelText": getattr(entity_info, "label_text", ""),
131
+ }
132
+ )
133
+
134
+ metadata_content = {
135
+ "type": "entities",
136
+ "count": len(entities),
137
+ "detailedItems": detailed_entities,
138
+ "totalEntities": len(entities),
139
+ "lastUpdated": datetime.utcnow().isoformat(),
140
+ "statistics": {
141
+ "readOnlyCount": sum(
142
+ 1 for e in detailed_entities if e["isReadOnly"]
143
+ ),
144
+ "writableCount": sum(
145
+ 1 for e in detailed_entities if not e["isReadOnly"]
146
+ ),
147
+ },
148
+ }
149
+
150
+ return json.dumps(metadata_content, indent=2)
151
+ except Exception as e:
152
+ logger.error(f"Failed to get entities metadata: {e}")
153
+ raise
154
+
155
+ async def _get_actions_metadata(self) -> str:
156
+ """Get actions metadata resource."""
157
+ try:
158
+ client = await self.client_manager.get_client()
159
+
160
+ # Get all actions
161
+ actions = await client.search_actions("")
162
+
163
+ # Get detailed info for first 50 actions (performance limit)
164
+ detailed_actions = []
165
+ for action_name in actions[:50]:
166
+ action_info = await client.get_action_info(action_name)
167
+ if action_info:
168
+ # Handle both object and dictionary return types
169
+ if isinstance(action_info, dict):
170
+ detailed_actions.append(
171
+ {
172
+ "name": action_info.get("name", action_name),
173
+ "isFunction": action_info.get("is_function", False),
174
+ "isBound": action_info.get("is_bound", False),
175
+ "parameterCount": len(
176
+ action_info.get("parameters", [])
177
+ ),
178
+ "returnType": action_info.get("return_type", "void"),
179
+ }
180
+ )
181
+ else:
182
+ detailed_actions.append(
183
+ {
184
+ "name": getattr(action_info, "name", action_name),
185
+ "isFunction": getattr(
186
+ action_info, "is_function", False
187
+ ),
188
+ "isBound": getattr(action_info, "is_bound", False),
189
+ "parameterCount": len(
190
+ getattr(action_info, "parameters", [])
191
+ ),
192
+ "returnType": getattr(
193
+ action_info, "return_type", "void"
194
+ ),
195
+ }
196
+ )
197
+
198
+ metadata_content = {
199
+ "type": "actions",
200
+ "count": len(actions),
201
+ "detailedItems": detailed_actions,
202
+ "totalActions": len(actions),
203
+ "lastUpdated": datetime.utcnow().isoformat(),
204
+ "statistics": {
205
+ "functionsCount": sum(
206
+ 1 for a in detailed_actions if a["isFunction"]
207
+ ),
208
+ "actionsCount": sum(
209
+ 1 for a in detailed_actions if not a["isFunction"]
210
+ ),
211
+ "boundCount": sum(1 for a in detailed_actions if a["isBound"]),
212
+ },
213
+ }
214
+
215
+ return json.dumps(metadata_content, indent=2)
216
+ except Exception as e:
217
+ logger.error(f"Failed to get actions metadata: {e}")
218
+ raise
219
+
220
+ async def _get_enumerations_metadata(self) -> str:
221
+ """Get enumerations metadata resource."""
222
+ try:
223
+ client = await self.client_manager.get_client()
224
+
225
+ # Try to get public enumerations
226
+ try:
227
+ enumerations = await client.get_public_enumerations()
228
+ detailed_enums = []
229
+
230
+ for enum_info in enumerations[:50]: # Limit for performance
231
+ # Handle both object and dictionary return types
232
+ if isinstance(enum_info, dict):
233
+ detailed_enums.append(
234
+ {
235
+ "name": enum_info.get("name", "Unknown"),
236
+ "valueCount": len(enum_info.get("members", [])),
237
+ "description": enum_info.get("description", ""),
238
+ }
239
+ )
240
+ else:
241
+ detailed_enums.append(
242
+ {
243
+ "name": getattr(enum_info, "name", "Unknown"),
244
+ "valueCount": len(getattr(enum_info, "members", [])),
245
+ "description": getattr(enum_info, "description", ""),
246
+ }
247
+ )
248
+ except Exception:
249
+ # Fallback if public enumerations not available
250
+ detailed_enums = []
251
+
252
+ metadata_content = {
253
+ "type": "enumerations",
254
+ "count": len(detailed_enums),
255
+ "detailedItems": detailed_enums,
256
+ "totalEnumerations": len(detailed_enums),
257
+ "lastUpdated": datetime.utcnow().isoformat(),
258
+ }
259
+
260
+ return json.dumps(metadata_content, indent=2)
261
+ except Exception as e:
262
+ logger.error(f"Failed to get enumerations metadata: {e}")
263
+ raise
264
+
265
+ async def _get_labels_metadata(self) -> str:
266
+ """Get labels metadata resource."""
267
+ try:
268
+ # For now, return basic label information
269
+ # In a full implementation, we'd query the label cache
270
+ metadata_content = {
271
+ "type": "labels",
272
+ "count": 0, # TODO: Get actual label count
273
+ "detailedItems": [],
274
+ "totalLabels": 0,
275
+ "lastUpdated": datetime.utcnow().isoformat(),
276
+ "supportedLanguages": ["en-US", "en-GB", "de-DE", "fr-FR", "es-ES"],
277
+ "note": "Label metadata requires metadata sync to be populated",
278
+ }
279
+
280
+ return json.dumps(metadata_content, indent=2)
281
+ except Exception as e:
282
+ logger.error(f"Failed to get labels metadata: {e}")
283
+ raise