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,135 @@
1
+ """Query 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 QueryResourceHandler:
16
+ """Handles query resources for the MCP server."""
17
+
18
+ def __init__(self, client_manager: D365FOClientManager):
19
+ """Initialize the query resource handler.
20
+
21
+ Args:
22
+ client_manager: D365FO client manager instance
23
+ """
24
+ self.client_manager = client_manager
25
+ # Predefined query templates
26
+ self.query_templates = {
27
+ "customers_recent": {
28
+ "entity_name": "Customers",
29
+ "description": "Recent customer records",
30
+ "select": ["CustomerAccount", "Name", "CreatedDateTime"],
31
+ "order_by": ["CreatedDateTime desc"],
32
+ "top": 50,
33
+ },
34
+ "sales_orders_today": {
35
+ "entity_name": "SalesOrders",
36
+ "description": "Sales orders created today",
37
+ "filter": "CreatedDateTime ge {today}",
38
+ "select": ["SalesOrderNumber", "CustomerAccount", "TotalAmount"],
39
+ "template": True,
40
+ "parameters": [
41
+ {
42
+ "name": "today",
43
+ "type": "datetime",
44
+ "required": True,
45
+ "description": "Today's date in ISO format",
46
+ }
47
+ ],
48
+ },
49
+ "vendors_active": {
50
+ "entity_name": "Vendors",
51
+ "description": "Active vendor records",
52
+ "filter": "Status eq 'Active'",
53
+ "select": ["VendorAccount", "Name", "PaymentTerms"],
54
+ "top": 100,
55
+ },
56
+ }
57
+
58
+ async def list_resources(self) -> List[Resource]:
59
+ """List available query resources.
60
+
61
+ Returns:
62
+ List of query resources
63
+ """
64
+ resources = []
65
+
66
+ for query_name, query_config in self.query_templates.items():
67
+ resources.append(
68
+ Resource(
69
+ uri=f"d365fo://queries/{query_name}",
70
+ name=f"Query: {query_name}",
71
+ description=query_config["description"],
72
+ mimeType="application/json",
73
+ )
74
+ )
75
+
76
+ logger.info(f"Listed {len(resources)} query resources")
77
+ return resources
78
+
79
+ async def read_resource(self, uri: str) -> str:
80
+ """Read specific query resource.
81
+
82
+ Args:
83
+ uri: Resource URI (e.g., "d365fo://queries/customers_recent")
84
+
85
+ Returns:
86
+ JSON string with query resource content
87
+ """
88
+ query_name = self._extract_query_name(uri)
89
+
90
+ try:
91
+ if query_name not in self.query_templates:
92
+ raise ValueError(f"Unknown query template: {query_name}")
93
+
94
+ query_config = self.query_templates[query_name]
95
+
96
+ # Build query resource content
97
+ resource_content = {
98
+ "queryName": query_name,
99
+ "entityName": query_config["entity_name"],
100
+ "description": query_config["description"],
101
+ "select": query_config.get("select"),
102
+ "filter": query_config.get("filter"),
103
+ "expand": query_config.get("expand"),
104
+ "orderBy": query_config.get("order_by"),
105
+ "top": query_config.get("top"),
106
+ "skip": query_config.get("skip"),
107
+ "template": query_config.get("template", False),
108
+ "parameters": query_config.get("parameters", []),
109
+ "lastUpdated": datetime.utcnow().isoformat(),
110
+ }
111
+
112
+ logger.info(f"Retrieved query resource: {query_name}")
113
+ return json.dumps(resource_content, indent=2)
114
+
115
+ except Exception as e:
116
+ logger.error(f"Failed to read query resource {query_name}: {e}")
117
+ error_content = {
118
+ "error": str(e),
119
+ "queryName": query_name,
120
+ "timestamp": datetime.utcnow().isoformat(),
121
+ }
122
+ return json.dumps(error_content, indent=2)
123
+
124
+ def _extract_query_name(self, uri: str) -> str:
125
+ """Extract query name from resource URI.
126
+
127
+ Args:
128
+ uri: Resource URI
129
+
130
+ Returns:
131
+ Query name
132
+ """
133
+ if uri.startswith("d365fo://queries/"):
134
+ return uri[len("d365fo://queries/") :]
135
+ raise ValueError(f"Invalid query resource URI: {uri}")
@@ -0,0 +1,432 @@
1
+ """Main MCP Server implementation for d365fo-client."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from mcp import GetPromptResult, Resource, Tool
9
+ from mcp.server import InitializationOptions, Server
10
+ from mcp.server.lowlevel.server import NotificationOptions
11
+ from mcp.types import Prompt, PromptArgument, PromptMessage, TextContent
12
+
13
+ from .client_manager import D365FOClientManager
14
+ from .models import MCPServerConfig
15
+ from .prompts import AVAILABLE_PROMPTS
16
+ from .resources import (
17
+ DatabaseResourceHandler,
18
+ EntityResourceHandler,
19
+ EnvironmentResourceHandler,
20
+ MetadataResourceHandler,
21
+ QueryResourceHandler,
22
+ )
23
+ from .tools import ConnectionTools, CrudTools, DatabaseTools, LabelTools, MetadataTools, ProfileTools
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class D365FOMCPServer:
29
+ """MCP Server for Microsoft Dynamics 365 Finance & Operations."""
30
+
31
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
32
+ """Initialize the MCP server.
33
+
34
+ Args:
35
+ config: Configuration dictionary
36
+ """
37
+ self.config = config or self._load_default_config()
38
+ self.server = Server("d365fo-mcp-server")
39
+ self.client_manager = D365FOClientManager(self.config)
40
+
41
+ # Initialize resource handlers
42
+ self.entity_handler = EntityResourceHandler(self.client_manager)
43
+ self.environment_handler = EnvironmentResourceHandler(self.client_manager)
44
+ self.metadata_handler = MetadataResourceHandler(self.client_manager)
45
+ self.query_handler = QueryResourceHandler(self.client_manager)
46
+ self.database_handler = DatabaseResourceHandler(self.client_manager)
47
+
48
+ # Initialize tool handlers
49
+ self.connection_tools = ConnectionTools(self.client_manager)
50
+ self.crud_tools = CrudTools(self.client_manager)
51
+ self.metadata_tools = MetadataTools(self.client_manager)
52
+ self.label_tools = LabelTools(self.client_manager)
53
+ self.profile_tools = ProfileTools(self.client_manager)
54
+ self.database_tools = DatabaseTools(self.client_manager)
55
+
56
+ # Tool registry for execution
57
+ self.tool_registry = {}
58
+
59
+ self._setup_handlers()
60
+
61
+ def _setup_handlers(self):
62
+ """Set up MCP server handlers."""
63
+
64
+ # Resource handlers
65
+ @self.server.list_resources()
66
+ async def handle_list_resources() -> List[Resource]:
67
+ """Handle list resources request."""
68
+ try:
69
+ resources = []
70
+
71
+ # Add entity resources
72
+ entity_resources = await self.entity_handler.list_resources()
73
+ resources.extend(entity_resources)
74
+
75
+ # Add environment resources
76
+ env_resources = await self.environment_handler.list_resources()
77
+ resources.extend(env_resources)
78
+
79
+ # Add metadata resources
80
+ metadata_resources = await self.metadata_handler.list_resources()
81
+ resources.extend(metadata_resources)
82
+
83
+ # Add query resources
84
+ query_resources = await self.query_handler.list_resources()
85
+ resources.extend(query_resources)
86
+
87
+ # Add database resources
88
+ database_resources = await self.database_handler.list_resources()
89
+ resources.extend(database_resources)
90
+
91
+ logger.info(f"Listed {len(resources)} total resources")
92
+ return resources
93
+ except Exception as e:
94
+ logger.error(f"Error listing resources: {e}")
95
+ return []
96
+
97
+ @self.server.read_resource()
98
+ async def handle_read_resource(uri: str) -> str:
99
+ """Handle read resource request."""
100
+ try:
101
+ if uri.startswith("d365fo://entities/"):
102
+ return await self.entity_handler.read_resource(uri)
103
+ elif uri.startswith("d365fo://environment/"):
104
+ return await self.environment_handler.read_resource(uri)
105
+ elif uri.startswith("d365fo://metadata/"):
106
+ return await self.metadata_handler.read_resource(uri)
107
+ elif uri.startswith("d365fo://queries/"):
108
+ return await self.query_handler.read_resource(uri)
109
+ elif uri.startswith("d365fo://database/"):
110
+ return await self.database_handler.read_resource(uri)
111
+ else:
112
+ raise ValueError(f"Unknown resource URI: {uri}")
113
+ except Exception as e:
114
+ logger.error(f"Error reading resource {uri}: {e}")
115
+ raise
116
+
117
+ # Prompt handlers
118
+ @self.server.list_prompts()
119
+ async def handle_list_prompts() -> List[Prompt]:
120
+ """Handle list prompts request."""
121
+ try:
122
+ logger.info("Handling list_prompts request")
123
+ prompts = []
124
+
125
+ for prompt_name, prompt_config in AVAILABLE_PROMPTS.items():
126
+ logger.info(f"Processing prompt: {prompt_name}")
127
+ # Convert our prompt config to MCP Prompt format
128
+ prompt_args = []
129
+ if hasattr(prompt_config.get("arguments"), "__annotations__"):
130
+ # Extract arguments from dataclass annotations
131
+ annotations = prompt_config["arguments"].__annotations__
132
+ for arg_name, arg_type in annotations.items():
133
+ prompt_args.append(
134
+ PromptArgument(
135
+ name=arg_name,
136
+ description=f"Parameter: {arg_name}",
137
+ required=False, # Make all optional for now
138
+ )
139
+ )
140
+
141
+ prompt = Prompt(
142
+ name=prompt_name,
143
+ description=prompt_config["description"],
144
+ arguments=prompt_args,
145
+ )
146
+ prompts.append(prompt)
147
+
148
+ logger.info(f"Listed {len(prompts)} prompts")
149
+ return prompts
150
+ except Exception as e:
151
+ logger.error(f"Error listing prompts: {e}")
152
+ return []
153
+
154
+ @self.server.get_prompt()
155
+ async def handle_get_prompt(
156
+ name: str, arguments: Optional[Dict[str, Any]] = None
157
+ ) -> GetPromptResult:
158
+ """Handle get prompt request."""
159
+ try:
160
+ logger.info(f"Handling get_prompt request for: {name}")
161
+ if name not in AVAILABLE_PROMPTS:
162
+ raise ValueError(f"Unknown prompt: {name}")
163
+
164
+ prompt_config = AVAILABLE_PROMPTS[name]
165
+ template = prompt_config["template"]
166
+
167
+ # For now, return the template as-is
168
+ # In the future, we could process arguments and customize the template
169
+ messages = [
170
+ PromptMessage(
171
+ role="user", content=TextContent(type="text", text=template)
172
+ )
173
+ ]
174
+
175
+ logger.info(f"Returning prompt template for: {name}")
176
+ return GetPromptResult(
177
+ description=prompt_config["description"], messages=messages
178
+ )
179
+ except Exception as e:
180
+ logger.error(f"Error getting prompt {name}: {e}")
181
+ raise
182
+
183
+ # Tool handlers
184
+ @self.server.list_tools()
185
+ async def handle_list_tools() -> List[Tool]:
186
+ """Handle list tools request."""
187
+ try:
188
+ tools = []
189
+
190
+ # Add connection tools
191
+ connection_tools = self.connection_tools.get_tools()
192
+ tools.extend(connection_tools)
193
+
194
+ # Add CRUD tools
195
+ crud_tools = self.crud_tools.get_tools()
196
+ tools.extend(crud_tools)
197
+
198
+ # Add metadata tools
199
+ metadata_tools = self.metadata_tools.get_tools()
200
+ tools.extend(metadata_tools)
201
+
202
+ # Add label tools
203
+ label_tools = self.label_tools.get_tools()
204
+ tools.extend(label_tools)
205
+
206
+ # Add profile tools
207
+ profile_tools = self.profile_tools.get_tools()
208
+ tools.extend(profile_tools)
209
+
210
+ # Add database tools
211
+ database_tools = self.database_tools.get_tools()
212
+ tools.extend(database_tools)
213
+
214
+ # Register tools for execution
215
+ for tool in tools:
216
+ self.tool_registry[tool.name] = tool
217
+
218
+ logger.info(f"Listed {len(tools)} tools")
219
+ return tools
220
+ except Exception as e:
221
+ logger.error(f"Error listing tools: {e}")
222
+ return []
223
+
224
+ @self.server.call_tool()
225
+ async def handle_call_tool(
226
+ name: str, arguments: Dict[str, Any]
227
+ ) -> List[TextContent]:
228
+ """Handle tool execution request."""
229
+ try:
230
+ logger.info(f"Executing tool: {name} with arguments: {arguments}")
231
+
232
+ # Route to appropriate tool handler
233
+ if name == "d365fo_test_connection":
234
+ return await self.connection_tools.execute_test_connection(
235
+ arguments
236
+ )
237
+ elif name == "d365fo_get_environment_info":
238
+ return await self.connection_tools.execute_get_environment_info(
239
+ arguments
240
+ )
241
+ elif name == "d365fo_query_entities":
242
+ return await self.crud_tools.execute_query_entities(arguments)
243
+ elif name == "d365fo_get_entity_record":
244
+ return await self.crud_tools.execute_get_entity_record(arguments)
245
+ elif name == "d365fo_create_entity_record":
246
+ return await self.crud_tools.execute_create_entity_record(arguments)
247
+ elif name == "d365fo_update_entity_record":
248
+ return await self.crud_tools.execute_update_entity_record(arguments)
249
+ elif name == "d365fo_delete_entity_record":
250
+ return await self.crud_tools.execute_delete_entity_record(arguments)
251
+ elif name == "d365fo_call_action":
252
+ return await self.crud_tools.execute_call_action(arguments)
253
+ elif name == "d365fo_search_entities":
254
+ return await self.metadata_tools.execute_search_entities(arguments)
255
+ elif name == "d365fo_get_entity_schema":
256
+ return await self.metadata_tools.execute_get_entity_schema(
257
+ arguments
258
+ )
259
+ elif name == "d365fo_search_actions":
260
+ return await self.metadata_tools.execute_search_actions(arguments)
261
+ elif name == "d365fo_search_enumerations":
262
+ return await self.metadata_tools.execute_search_enumerations(
263
+ arguments
264
+ )
265
+ elif name == "d365fo_get_enumeration_fields":
266
+ return await self.metadata_tools.execute_get_enumeration_fields(
267
+ arguments
268
+ )
269
+ elif name == "d365fo_get_installed_modules":
270
+ return await self.metadata_tools.execute_get_installed_modules(
271
+ arguments
272
+ )
273
+ elif name == "d365fo_get_label":
274
+ return await self.label_tools.execute_get_label(arguments)
275
+ elif name == "d365fo_get_labels_batch":
276
+ return await self.label_tools.execute_get_labels_batch(arguments)
277
+ elif name == "d365fo_list_profiles":
278
+ return await self.profile_tools.execute_list_profiles(arguments)
279
+ elif name == "d365fo_get_profile":
280
+ return await self.profile_tools.execute_get_profile(arguments)
281
+ elif name == "d365fo_create_profile":
282
+ return await self.profile_tools.execute_create_profile(arguments)
283
+ elif name == "d365fo_update_profile":
284
+ return await self.profile_tools.execute_update_profile(arguments)
285
+ elif name == "d365fo_delete_profile":
286
+ return await self.profile_tools.execute_delete_profile(arguments)
287
+ elif name == "d365fo_set_default_profile":
288
+ return await self.profile_tools.execute_set_default_profile(
289
+ arguments
290
+ )
291
+ elif name == "d365fo_get_default_profile":
292
+ return await self.profile_tools.execute_get_default_profile(
293
+ arguments
294
+ )
295
+ elif name == "d365fo_validate_profile":
296
+ return await self.profile_tools.execute_validate_profile(arguments)
297
+ elif name == "d365fo_test_profile_connection":
298
+ return await self.profile_tools.execute_test_profile_connection(
299
+ arguments
300
+ )
301
+ elif name == "d365fo_execute_sql_query":
302
+ return await self.database_tools.execute_sql_query(arguments)
303
+ elif name == "d365fo_get_database_schema":
304
+ return await self.database_tools.execute_get_database_schema(arguments)
305
+ elif name == "d365fo_get_table_info":
306
+ return await self.database_tools.execute_get_table_info(arguments)
307
+ elif name == "d365fo_get_database_statistics":
308
+ return await self.database_tools.execute_get_database_statistics(arguments)
309
+ else:
310
+ raise ValueError(f"Unknown tool: {name}")
311
+
312
+ except Exception as e:
313
+ logger.error(f"Error executing tool {name}: {e}")
314
+ error_response = {"error": str(e), "tool": name, "arguments": arguments}
315
+ return [TextContent(type="text", text=str(error_response))]
316
+
317
+ async def run(self, transport_type: str = "stdio"):
318
+ """Run the MCP server.
319
+
320
+ Args:
321
+ transport_type: Transport type (stdio, sse, etc.)
322
+ """
323
+ try:
324
+ logger.info("Starting D365FO MCP Server...")
325
+
326
+ # Perform health checks
327
+ await self._startup_health_checks()
328
+
329
+ if transport_type == "stdio":
330
+ from mcp.server.stdio import stdio_server
331
+
332
+ async with stdio_server() as (read_stream, write_stream):
333
+ await self.server.run(
334
+ read_stream,
335
+ write_stream,
336
+ InitializationOptions(
337
+ server_name="d365fo-mcp-server",
338
+ server_version="1.0.0",
339
+ capabilities=self.server.get_capabilities(
340
+ notification_options=NotificationOptions(),
341
+ experimental_capabilities={},
342
+ ),
343
+ ),
344
+ )
345
+ else:
346
+ raise ValueError(f"Unsupported transport type: {transport_type}")
347
+
348
+ except Exception as e:
349
+ logger.error(f"Error running MCP server: {e}")
350
+ raise
351
+ finally:
352
+ await self.cleanup()
353
+
354
+ async def _startup_health_checks(self):
355
+ """Perform startup health checks."""
356
+ try:
357
+ logger.info("Performing startup health checks...")
358
+
359
+ # Test default connection
360
+ connection_ok = await self.client_manager.test_connection()
361
+ if not connection_ok:
362
+ logger.warning("Default connection test failed during startup")
363
+ else:
364
+ logger.info("Default connection test passed")
365
+
366
+ # Get environment info to verify functionality
367
+ try:
368
+ env_info = await self.client_manager.get_environment_info()
369
+ logger.info(f"Connected to D365FO environment: {env_info['base_url']}")
370
+ logger.info(
371
+ f"Application version: {env_info['versions']['application']}"
372
+ )
373
+ except Exception as e:
374
+ logger.warning(f"Could not retrieve environment info: {e}")
375
+
376
+ except Exception as e:
377
+ logger.error(f"Startup health checks failed: {e}")
378
+ # Don't fail startup on health check failures
379
+
380
+ async def cleanup(self):
381
+ """Clean up resources."""
382
+ try:
383
+ logger.info("Cleaning up D365FO MCP Server...")
384
+ await self.client_manager.cleanup()
385
+ except Exception as e:
386
+ logger.error(f"Error during cleanup: {e}")
387
+
388
+ def _load_default_config(self) -> Dict[str, Any]:
389
+ """Load default configuration.
390
+
391
+ Returns:
392
+ Default configuration dictionary
393
+ """
394
+ return {
395
+ "default_environment": {
396
+ "base_url": os.getenv(
397
+ "D365FO_BASE_URL",
398
+ "https://usnconeboxax1aos.cloud.onebox.dynamics.com",
399
+ ),
400
+ "use_default_credentials": True,
401
+ "use_cache_first": True,
402
+ "timeout": 60,
403
+ "verify_ssl": True,
404
+ "use_label_cache": True,
405
+ },
406
+ "cache": {
407
+ "metadata_cache_dir": os.path.expanduser("~/.d365fo-mcp/cache"),
408
+ "label_cache_expiry_minutes": 120,
409
+ "use_label_cache": True,
410
+ "cache_size_limit_mb": 100,
411
+ },
412
+ "performance": {
413
+ "max_concurrent_requests": 10,
414
+ "connection_pool_size": 5,
415
+ "request_timeout": 30,
416
+ "batch_size": 100,
417
+ },
418
+ "security": {
419
+ "encrypt_cached_tokens": True,
420
+ "token_expiry_buffer_minutes": 5,
421
+ "max_retry_attempts": 3,
422
+ },
423
+ "profiles": {
424
+ "default": {
425
+ "base_url": os.getenv(
426
+ "D365FO_BASE_URL",
427
+ "https://usnconeboxax1aos.cloud.onebox.dynamics.com",
428
+ ),
429
+ "use_default_credentials": True,
430
+ }
431
+ },
432
+ }
@@ -0,0 +1,17 @@
1
+ """Tools package for MCP server."""
2
+
3
+ from .connection_tools import ConnectionTools
4
+ from .crud_tools import CrudTools
5
+ from .database_tools import DatabaseTools
6
+ from .label_tools import LabelTools
7
+ from .metadata_tools import MetadataTools
8
+ from .profile_tools import ProfileTools
9
+
10
+ __all__ = [
11
+ "ConnectionTools",
12
+ "MetadataTools",
13
+ "CrudTools",
14
+ "LabelTools",
15
+ "ProfileTools",
16
+ "DatabaseTools",
17
+ ]