d365fo-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- d365fo_client/__init__.py +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
|
+
]
|