d365fo-client 0.3.1__tar.gz → 0.3.2__tar.gz
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-0.3.1/src/d365fo_client.egg-info → d365fo_client-0.3.2}/PKG-INFO +114 -3
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/README.md +112 -1
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/pyproject.toml +2 -2
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/cli.py +135 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/client.py +371 -19
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/crud.py +33 -10
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/main.py +58 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/fastmcp_main.py +1 -1
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/fastmcp_utils.py +4 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/base_tools_mixin.py +13 -12
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/crud_tools_mixin.py +181 -40
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/server.py +10 -1
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/__init__.py +2 -0
- d365fo_client-0.3.2/src/d365fo_client/mcp/tools/json_service_tools.py +326 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/models.py +45 -0
- d365fo_client-0.3.2/src/d365fo_client/odata_serializer.py +300 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/query.py +30 -20
- {d365fo_client-0.3.1 → d365fo_client-0.3.2/src/d365fo_client.egg-info}/PKG-INFO +114 -3
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/SOURCES.txt +2 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/requires.txt +1 -1
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/LICENSE +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/setup.cfg +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/auth.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/config.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/credential_sources.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/exceptions.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/labels.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/auth.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/oauth_proxy.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/apikey.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/azure.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/bearer.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/jwt.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/redirect_validation.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/dependencies.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/client_manager.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/fastmcp_server.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/main.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/connection_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/database_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/label_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/metadata_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/performance_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/profile_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/sync_tools_mixin.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/models.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/prompts/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/prompts/action_execution.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/prompts/sequence_analysis.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/database_handler.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/entity_handler.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/environment_handler.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/metadata_handler.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/query_handler.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/connection_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/crud_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/database_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/label_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/metadata_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/profile_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/sync_tools.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/auth.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/logging.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/types.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_api.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/__init__.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/cache_v2.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/database_v2.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/global_version_manager.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/label_utils.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/search_engine_v2.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/sync_manager_v2.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/sync_session_manager.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/version_detector.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/output.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/profile_manager.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/profiles.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/session.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/settings.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/sync_models.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/utils.py +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/dependency_links.txt +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/entry_points.txt +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/top_level.txt +0 -0
- {d365fo_client-0.3.1 → d365fo_client-0.3.2}/tests/test_azure_provider_persistence.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: d365fo-client
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.2
|
4
4
|
Summary: Microsoft Dynamics 365 Finance & Operations client
|
5
5
|
Author-email: Muhammad Afzaal <mo@thedataguy.pro>
|
6
6
|
License-Expression: MIT
|
@@ -30,7 +30,7 @@ Requires-Dist: diskcache>=5.6.3
|
|
30
30
|
Requires-Dist: tabulate>=0.9.0
|
31
31
|
Requires-Dist: pyyaml>=6.0
|
32
32
|
Requires-Dist: mcp>=1.13.0
|
33
|
-
Requires-Dist: uvicorn
|
33
|
+
Requires-Dist: uvicorn>=0.32.0
|
34
34
|
Requires-Dist: pydantic-settings>=2.6.0
|
35
35
|
Requires-Dist: authlib>=1.6.4
|
36
36
|
Requires-Dist: httpx>=0.28.1
|
@@ -59,6 +59,34 @@ Dynamic: license-file
|
|
59
59
|
[-2496ED?style=flat-square&logo=docker&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=d365fo-docker&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22-v%22%2C%22d365fo-mcp%3A%2Fhome%2Fmcp_user%2F%22%2C%22-e%22%2C%22D365FO_CLIENT_ID%3D%24%7Binput%3Aclient_id%7D%22%2C%22-e%22%2C%22D365FO_CLIENT_SECRET%3D%24%7Binput%3Aclient_secret%7D%22%2C%22-e%22%2C%22D365FO_TENANT_ID%3D%24%7Binput%3Atenant_id%7D%22%2C%22ghcr.io%2Fmafzaal%2Fd365fo-client%3Alatest%22%5D%2C%22env%22%3A%7B%22D365FO_LOG_LEVEL%22%3A%22DEBUG%22%2C%22D365FO_CLIENT_ID%22%3A%22%24%7Binput%3Aclient_id%7D%22%2C%22D365FO_CLIENT_SECRET%22%3A%22%24%7Binput%3Aclient_secret%7D%22%2C%22D365FO_TENANT_ID%22%3A%22%24%7Binput%3Atenant_id%7D%22%7D%7D&inputs=%5B%7B%22id%22%3A%22tenant_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Tenant%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_secret%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20Secret%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%5D)
|
60
60
|
[-2496ED?style=flat-square&logo=docker&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=d365fo-docker&quality=insiders&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22-v%22%2C%22d365fo-mcp%3A%2Fhome%2Fmcp_user%2F%22%2C%22-e%22%2C%22D365FO_CLIENT_ID%3D%24%7Binput%3Aclient_id%7D%22%2C%22-e%22%2C%22D365FO_CLIENT_SECRET%3D%24%7Binput%3Aclient_secret%7D%22%2C%22-e%22%2C%22D365FO_TENANT_ID%3D%24%7Binput%3Atenant_id%7D%22%2C%22ghcr.io%2Fmafzaal%2Fd365fo-client%3Alatest%22%5D%2C%22env%22%3A%7B%22D365FO_LOG_LEVEL%22%3A%22DEBUG%22%2C%22D365FO_CLIENT_ID%22%3A%22%24%7Binput%3Aclient_id%7D%22%2C%22D365FO_CLIENT_SECRET%22%3A%22%24%7Binput%3Aclient_secret%7D%22%2C%22D365FO_TENANT_ID%22%3A%22%24%7Binput%3Atenant_id%7D%22%7D%7D&inputs=%5B%7B%22id%22%3A%22tenant_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Tenant%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_secret%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20Secret%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%5D)
|
61
61
|
|
62
|
+
**☁️ Deploy to Azure Container Apps:**
|
63
|
+
|
64
|
+
Deploy the MCP server as a secure, internet-accessible HTTP endpoint with OAuth or API Key authentication. Perfect for web integrations and remote AI assistant access.
|
65
|
+
|
66
|
+
**Option 1: Using Bash Script (Recommended)**
|
67
|
+
```bash
|
68
|
+
# Download and run the deployment script
|
69
|
+
curl -O https://raw.githubusercontent.com/mafzaal/d365fo-client/main/deploy-aca.sh
|
70
|
+
chmod +x deploy-aca.sh
|
71
|
+
|
72
|
+
# Set authentication (choose OAuth or API Key)
|
73
|
+
export D365FO_MCP_AUTH_CLIENT_ID="your-client-id"
|
74
|
+
export D365FO_MCP_AUTH_CLIENT_SECRET="your-client-secret"
|
75
|
+
export D365FO_MCP_AUTH_TENANT_ID="your-tenant-id"
|
76
|
+
# OR
|
77
|
+
export D365FO_MCP_API_KEY_VALUE="your-secret-key"
|
78
|
+
|
79
|
+
# Deploy
|
80
|
+
./deploy-aca.sh
|
81
|
+
```
|
82
|
+
|
83
|
+
**Option 2: Using ARM Template**
|
84
|
+
1. Download [azure-deploy.json](https://raw.githubusercontent.com/mafzaal/d365fo-client/main/azure-deploy.json)
|
85
|
+
2. Go to [Azure Portal → Deploy a custom template](https://portal.azure.com/#create/Microsoft.Template)
|
86
|
+
3. Click "Build your own template in the editor"
|
87
|
+
4. Paste the contents of `azure-deploy.json`
|
88
|
+
5. Fill in the parameters and deploy
|
89
|
+
|
62
90
|
[](https://pypi.org/project/d365fo-client/)
|
63
91
|
|
64
92
|
**Also includes a comprehensive Python client library** for Microsoft Dynamics 365 Finance & Operations with OData endpoints, metadata operations, label management, and CLI tools.
|
@@ -822,7 +850,8 @@ export DEBUG="true" # Enable debug mode
|
|
822
850
|
- 🏷️ **Label Operations V2**: Multilingual label caching with performance improvements and async support
|
823
851
|
- 🔍 **Advanced Querying**: Support for all OData query parameters ($select, $filter, $expand, etc.)
|
824
852
|
- ⚡ **Action Execution**: Execute bound and unbound OData actions with comprehensive parameter handling
|
825
|
-
-
|
853
|
+
- �️ **JSON Services**: Generic access to D365 F&O JSON service endpoints (/api/services pattern)
|
854
|
+
- �🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
|
826
855
|
- 💾 **Intelligent Caching**: Cross-environment cache sharing with module-based version detection
|
827
856
|
- 🌐 **Async/Await**: Modern async/await patterns with optimized session management
|
828
857
|
- 📝 **Type Hints**: Full type annotation support with enhanced data models
|
@@ -933,6 +962,18 @@ d365fo-client labels resolve "@SYS13342"
|
|
933
962
|
d365fo-client labels search "customer" --language "en-US"
|
934
963
|
```
|
935
964
|
|
965
|
+
#### JSON Service Operations
|
966
|
+
```bash
|
967
|
+
# Call SQL diagnostic services
|
968
|
+
d365fo-client service sql-diagnostic GetAxSqlExecuting
|
969
|
+
d365fo-client service sql-diagnostic GetAxSqlResourceStats --since-minutes 5
|
970
|
+
d365fo-client service sql-diagnostic GetAxSqlBlocking --output json
|
971
|
+
|
972
|
+
# Generic JSON service calls
|
973
|
+
d365fo-client service call SysSqlDiagnosticService SysSqlDiagnosticServiceOperations GetAxSqlExecuting
|
974
|
+
d365fo-client service call YourServiceGroup YourServiceName YourOperation --parameters '{"param1":"value1"}'
|
975
|
+
```
|
976
|
+
|
936
977
|
### Global Options
|
937
978
|
|
938
979
|
- `--base-url URL` — Specify D365 F&O environment URL
|
@@ -1189,6 +1230,76 @@ result = await client.post_data("/data/CustomersV3('US-001')/calculateBalance",
|
|
1189
1230
|
})
|
1190
1231
|
```
|
1191
1232
|
|
1233
|
+
### JSON Service Operations
|
1234
|
+
|
1235
|
+
```python
|
1236
|
+
# Basic JSON service call (no parameters)
|
1237
|
+
response = await client.post_json_service(
|
1238
|
+
service_group="SysSqlDiagnosticService",
|
1239
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1240
|
+
operation_name="GetAxSqlExecuting"
|
1241
|
+
)
|
1242
|
+
|
1243
|
+
if response.success:
|
1244
|
+
print(f"Found {len(response.data)} executing SQL statements")
|
1245
|
+
print(f"Status: HTTP {response.status_code}")
|
1246
|
+
else:
|
1247
|
+
print(f"Error: {response.error_message}")
|
1248
|
+
|
1249
|
+
# JSON service call with parameters
|
1250
|
+
from datetime import datetime, timezone, timedelta
|
1251
|
+
|
1252
|
+
end_time = datetime.now(timezone.utc)
|
1253
|
+
start_time = end_time - timedelta(minutes=10)
|
1254
|
+
|
1255
|
+
response = await client.post_json_service(
|
1256
|
+
service_group="SysSqlDiagnosticService",
|
1257
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1258
|
+
operation_name="GetAxSqlResourceStats",
|
1259
|
+
parameters={
|
1260
|
+
"start": start_time.isoformat(),
|
1261
|
+
"end": end_time.isoformat()
|
1262
|
+
}
|
1263
|
+
)
|
1264
|
+
|
1265
|
+
# Using JsonServiceRequest object for better structure
|
1266
|
+
from d365fo_client.models import JsonServiceRequest
|
1267
|
+
|
1268
|
+
request = JsonServiceRequest(
|
1269
|
+
service_group="SysSqlDiagnosticService",
|
1270
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1271
|
+
operation_name="GetAxSqlBlocking"
|
1272
|
+
)
|
1273
|
+
|
1274
|
+
response = await client.call_json_service(request)
|
1275
|
+
print(f"Service endpoint: {request.get_endpoint_path()}")
|
1276
|
+
|
1277
|
+
# Multiple SQL diagnostic operations
|
1278
|
+
operations = ["GetAxSqlExecuting", "GetAxSqlBlocking", "GetAxSqlLockInfo"]
|
1279
|
+
for operation in operations:
|
1280
|
+
response = await client.post_json_service(
|
1281
|
+
service_group="SysSqlDiagnosticService",
|
1282
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1283
|
+
operation_name=operation
|
1284
|
+
)
|
1285
|
+
|
1286
|
+
if response.success:
|
1287
|
+
count = len(response.data) if isinstance(response.data, list) else 1
|
1288
|
+
print(f"{operation}: {count} records")
|
1289
|
+
|
1290
|
+
# Custom service call template
|
1291
|
+
response = await client.post_json_service(
|
1292
|
+
service_group="YourServiceGroup",
|
1293
|
+
service_name="YourServiceName",
|
1294
|
+
operation_name="YourOperation",
|
1295
|
+
parameters={
|
1296
|
+
"parameter1": "value1",
|
1297
|
+
"parameter2": 123,
|
1298
|
+
"parameter3": True
|
1299
|
+
}
|
1300
|
+
)
|
1301
|
+
```
|
1302
|
+
|
1192
1303
|
### Metadata Operations
|
1193
1304
|
|
1194
1305
|
```python
|
@@ -12,6 +12,34 @@
|
|
12
12
|
[-2496ED?style=flat-square&logo=docker&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=d365fo-docker&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22-v%22%2C%22d365fo-mcp%3A%2Fhome%2Fmcp_user%2F%22%2C%22-e%22%2C%22D365FO_CLIENT_ID%3D%24%7Binput%3Aclient_id%7D%22%2C%22-e%22%2C%22D365FO_CLIENT_SECRET%3D%24%7Binput%3Aclient_secret%7D%22%2C%22-e%22%2C%22D365FO_TENANT_ID%3D%24%7Binput%3Atenant_id%7D%22%2C%22ghcr.io%2Fmafzaal%2Fd365fo-client%3Alatest%22%5D%2C%22env%22%3A%7B%22D365FO_LOG_LEVEL%22%3A%22DEBUG%22%2C%22D365FO_CLIENT_ID%22%3A%22%24%7Binput%3Aclient_id%7D%22%2C%22D365FO_CLIENT_SECRET%22%3A%22%24%7Binput%3Aclient_secret%7D%22%2C%22D365FO_TENANT_ID%22%3A%22%24%7Binput%3Atenant_id%7D%22%7D%7D&inputs=%5B%7B%22id%22%3A%22tenant_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Tenant%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_secret%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20Secret%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%5D)
|
13
13
|
[-2496ED?style=flat-square&logo=docker&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=d365fo-docker&quality=insiders&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22-v%22%2C%22d365fo-mcp%3A%2Fhome%2Fmcp_user%2F%22%2C%22-e%22%2C%22D365FO_CLIENT_ID%3D%24%7Binput%3Aclient_id%7D%22%2C%22-e%22%2C%22D365FO_CLIENT_SECRET%3D%24%7Binput%3Aclient_secret%7D%22%2C%22-e%22%2C%22D365FO_TENANT_ID%3D%24%7Binput%3Atenant_id%7D%22%2C%22ghcr.io%2Fmafzaal%2Fd365fo-client%3Alatest%22%5D%2C%22env%22%3A%7B%22D365FO_LOG_LEVEL%22%3A%22DEBUG%22%2C%22D365FO_CLIENT_ID%22%3A%22%24%7Binput%3Aclient_id%7D%22%2C%22D365FO_CLIENT_SECRET%22%3A%22%24%7Binput%3Aclient_secret%7D%22%2C%22D365FO_TENANT_ID%22%3A%22%24%7Binput%3Atenant_id%7D%22%7D%7D&inputs=%5B%7B%22id%22%3A%22tenant_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Tenant%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_id%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20ID%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%2C%7B%22id%22%3A%22client_secret%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Azure%20AD%20Client%20Secret%20for%20D365%20F%26O%20authentication%22%2C%22password%22%3Atrue%7D%5D)
|
14
14
|
|
15
|
+
**☁️ Deploy to Azure Container Apps:**
|
16
|
+
|
17
|
+
Deploy the MCP server as a secure, internet-accessible HTTP endpoint with OAuth or API Key authentication. Perfect for web integrations and remote AI assistant access.
|
18
|
+
|
19
|
+
**Option 1: Using Bash Script (Recommended)**
|
20
|
+
```bash
|
21
|
+
# Download and run the deployment script
|
22
|
+
curl -O https://raw.githubusercontent.com/mafzaal/d365fo-client/main/deploy-aca.sh
|
23
|
+
chmod +x deploy-aca.sh
|
24
|
+
|
25
|
+
# Set authentication (choose OAuth or API Key)
|
26
|
+
export D365FO_MCP_AUTH_CLIENT_ID="your-client-id"
|
27
|
+
export D365FO_MCP_AUTH_CLIENT_SECRET="your-client-secret"
|
28
|
+
export D365FO_MCP_AUTH_TENANT_ID="your-tenant-id"
|
29
|
+
# OR
|
30
|
+
export D365FO_MCP_API_KEY_VALUE="your-secret-key"
|
31
|
+
|
32
|
+
# Deploy
|
33
|
+
./deploy-aca.sh
|
34
|
+
```
|
35
|
+
|
36
|
+
**Option 2: Using ARM Template**
|
37
|
+
1. Download [azure-deploy.json](https://raw.githubusercontent.com/mafzaal/d365fo-client/main/azure-deploy.json)
|
38
|
+
2. Go to [Azure Portal → Deploy a custom template](https://portal.azure.com/#create/Microsoft.Template)
|
39
|
+
3. Click "Build your own template in the editor"
|
40
|
+
4. Paste the contents of `azure-deploy.json`
|
41
|
+
5. Fill in the parameters and deploy
|
42
|
+
|
15
43
|
[](https://pypi.org/project/d365fo-client/)
|
16
44
|
|
17
45
|
**Also includes a comprehensive Python client library** for Microsoft Dynamics 365 Finance & Operations with OData endpoints, metadata operations, label management, and CLI tools.
|
@@ -775,7 +803,8 @@ export DEBUG="true" # Enable debug mode
|
|
775
803
|
- 🏷️ **Label Operations V2**: Multilingual label caching with performance improvements and async support
|
776
804
|
- 🔍 **Advanced Querying**: Support for all OData query parameters ($select, $filter, $expand, etc.)
|
777
805
|
- ⚡ **Action Execution**: Execute bound and unbound OData actions with comprehensive parameter handling
|
778
|
-
-
|
806
|
+
- �️ **JSON Services**: Generic access to D365 F&O JSON service endpoints (/api/services pattern)
|
807
|
+
- �🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
|
779
808
|
- 💾 **Intelligent Caching**: Cross-environment cache sharing with module-based version detection
|
780
809
|
- 🌐 **Async/Await**: Modern async/await patterns with optimized session management
|
781
810
|
- 📝 **Type Hints**: Full type annotation support with enhanced data models
|
@@ -886,6 +915,18 @@ d365fo-client labels resolve "@SYS13342"
|
|
886
915
|
d365fo-client labels search "customer" --language "en-US"
|
887
916
|
```
|
888
917
|
|
918
|
+
#### JSON Service Operations
|
919
|
+
```bash
|
920
|
+
# Call SQL diagnostic services
|
921
|
+
d365fo-client service sql-diagnostic GetAxSqlExecuting
|
922
|
+
d365fo-client service sql-diagnostic GetAxSqlResourceStats --since-minutes 5
|
923
|
+
d365fo-client service sql-diagnostic GetAxSqlBlocking --output json
|
924
|
+
|
925
|
+
# Generic JSON service calls
|
926
|
+
d365fo-client service call SysSqlDiagnosticService SysSqlDiagnosticServiceOperations GetAxSqlExecuting
|
927
|
+
d365fo-client service call YourServiceGroup YourServiceName YourOperation --parameters '{"param1":"value1"}'
|
928
|
+
```
|
929
|
+
|
889
930
|
### Global Options
|
890
931
|
|
891
932
|
- `--base-url URL` — Specify D365 F&O environment URL
|
@@ -1142,6 +1183,76 @@ result = await client.post_data("/data/CustomersV3('US-001')/calculateBalance",
|
|
1142
1183
|
})
|
1143
1184
|
```
|
1144
1185
|
|
1186
|
+
### JSON Service Operations
|
1187
|
+
|
1188
|
+
```python
|
1189
|
+
# Basic JSON service call (no parameters)
|
1190
|
+
response = await client.post_json_service(
|
1191
|
+
service_group="SysSqlDiagnosticService",
|
1192
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1193
|
+
operation_name="GetAxSqlExecuting"
|
1194
|
+
)
|
1195
|
+
|
1196
|
+
if response.success:
|
1197
|
+
print(f"Found {len(response.data)} executing SQL statements")
|
1198
|
+
print(f"Status: HTTP {response.status_code}")
|
1199
|
+
else:
|
1200
|
+
print(f"Error: {response.error_message}")
|
1201
|
+
|
1202
|
+
# JSON service call with parameters
|
1203
|
+
from datetime import datetime, timezone, timedelta
|
1204
|
+
|
1205
|
+
end_time = datetime.now(timezone.utc)
|
1206
|
+
start_time = end_time - timedelta(minutes=10)
|
1207
|
+
|
1208
|
+
response = await client.post_json_service(
|
1209
|
+
service_group="SysSqlDiagnosticService",
|
1210
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1211
|
+
operation_name="GetAxSqlResourceStats",
|
1212
|
+
parameters={
|
1213
|
+
"start": start_time.isoformat(),
|
1214
|
+
"end": end_time.isoformat()
|
1215
|
+
}
|
1216
|
+
)
|
1217
|
+
|
1218
|
+
# Using JsonServiceRequest object for better structure
|
1219
|
+
from d365fo_client.models import JsonServiceRequest
|
1220
|
+
|
1221
|
+
request = JsonServiceRequest(
|
1222
|
+
service_group="SysSqlDiagnosticService",
|
1223
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1224
|
+
operation_name="GetAxSqlBlocking"
|
1225
|
+
)
|
1226
|
+
|
1227
|
+
response = await client.call_json_service(request)
|
1228
|
+
print(f"Service endpoint: {request.get_endpoint_path()}")
|
1229
|
+
|
1230
|
+
# Multiple SQL diagnostic operations
|
1231
|
+
operations = ["GetAxSqlExecuting", "GetAxSqlBlocking", "GetAxSqlLockInfo"]
|
1232
|
+
for operation in operations:
|
1233
|
+
response = await client.post_json_service(
|
1234
|
+
service_group="SysSqlDiagnosticService",
|
1235
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
1236
|
+
operation_name=operation
|
1237
|
+
)
|
1238
|
+
|
1239
|
+
if response.success:
|
1240
|
+
count = len(response.data) if isinstance(response.data, list) else 1
|
1241
|
+
print(f"{operation}: {count} records")
|
1242
|
+
|
1243
|
+
# Custom service call template
|
1244
|
+
response = await client.post_json_service(
|
1245
|
+
service_group="YourServiceGroup",
|
1246
|
+
service_name="YourServiceName",
|
1247
|
+
operation_name="YourOperation",
|
1248
|
+
parameters={
|
1249
|
+
"parameter1": "value1",
|
1250
|
+
"parameter2": 123,
|
1251
|
+
"parameter3": True
|
1252
|
+
}
|
1253
|
+
)
|
1254
|
+
```
|
1255
|
+
|
1145
1256
|
### Metadata Operations
|
1146
1257
|
|
1147
1258
|
```python
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "d365fo-client"
|
3
|
-
version = "0.3.
|
3
|
+
version = "0.3.2"
|
4
4
|
description = "Microsoft Dynamics 365 Finance & Operations client"
|
5
5
|
readme = "README.md"
|
6
6
|
license = "MIT"
|
@@ -18,7 +18,7 @@ dependencies = [
|
|
18
18
|
"tabulate>=0.9.0",
|
19
19
|
"pyyaml>=6.0",
|
20
20
|
"mcp>=1.13.0",
|
21
|
-
"uvicorn
|
21
|
+
"uvicorn>=0.32.0",
|
22
22
|
"pydantic-settings>=2.6.0",
|
23
23
|
"authlib>=1.6.4",
|
24
24
|
"httpx>=0.28.1",
|
@@ -99,6 +99,7 @@ class CLIManager:
|
|
99
99
|
"metadata": self._handle_metadata_commands,
|
100
100
|
"entity": self._handle_entity_commands,
|
101
101
|
"action": self._handle_action_commands,
|
102
|
+
"service": self._handle_service_commands,
|
102
103
|
}
|
103
104
|
|
104
105
|
command = getattr(args, "command", None)
|
@@ -694,6 +695,140 @@ class CLIManager:
|
|
694
695
|
print(format_error_message(f"Error setting default profile: {e}"))
|
695
696
|
return 1
|
696
697
|
|
698
|
+
async def _handle_service_commands(self, args: argparse.Namespace) -> int:
|
699
|
+
"""Handle JSON service commands."""
|
700
|
+
subcommand = getattr(args, "service_subcommand", None)
|
701
|
+
|
702
|
+
if subcommand == "call":
|
703
|
+
return await self._handle_service_call(args)
|
704
|
+
elif subcommand == "sql-diagnostic":
|
705
|
+
return await self._handle_service_sql_diagnostic(args)
|
706
|
+
else:
|
707
|
+
print(format_error_message(f"Unknown service subcommand: {subcommand}"))
|
708
|
+
return 1
|
709
|
+
|
710
|
+
async def _handle_service_call(self, args: argparse.Namespace) -> int:
|
711
|
+
"""Handle generic JSON service call command."""
|
712
|
+
try:
|
713
|
+
service_group = getattr(args, "service_group", "")
|
714
|
+
service_name = getattr(args, "service_name", "")
|
715
|
+
operation_name = getattr(args, "operation_name", "")
|
716
|
+
|
717
|
+
# Parse parameters from JSON string if provided
|
718
|
+
parameters = None
|
719
|
+
parameters_str = getattr(args, "parameters", None)
|
720
|
+
if parameters_str:
|
721
|
+
try:
|
722
|
+
parameters = json.loads(parameters_str)
|
723
|
+
except json.JSONDecodeError as e:
|
724
|
+
print(format_error_message(f"Invalid JSON in parameters: {e}"))
|
725
|
+
return 1
|
726
|
+
|
727
|
+
# Call the service
|
728
|
+
response = await self.client.post_json_service(
|
729
|
+
service_group=service_group,
|
730
|
+
service_name=service_name,
|
731
|
+
operation_name=operation_name,
|
732
|
+
parameters=parameters,
|
733
|
+
)
|
734
|
+
|
735
|
+
# Format and display response
|
736
|
+
if response.success:
|
737
|
+
result = {
|
738
|
+
"success": True,
|
739
|
+
"statusCode": response.status_code,
|
740
|
+
"data": response.data,
|
741
|
+
"serviceGroup": service_group,
|
742
|
+
"serviceName": service_name,
|
743
|
+
"operationName": operation_name,
|
744
|
+
}
|
745
|
+
output = self.output_formatter.format_output(result)
|
746
|
+
print(output)
|
747
|
+
return 0
|
748
|
+
else:
|
749
|
+
error_result = {
|
750
|
+
"success": False,
|
751
|
+
"statusCode": response.status_code,
|
752
|
+
"error": response.error_message,
|
753
|
+
"serviceGroup": service_group,
|
754
|
+
"serviceName": service_name,
|
755
|
+
"operationName": operation_name,
|
756
|
+
}
|
757
|
+
output = self.output_formatter.format_output(error_result)
|
758
|
+
print(output)
|
759
|
+
return 1
|
760
|
+
|
761
|
+
except Exception as e:
|
762
|
+
print(format_error_message(f"Error calling service: {e}"))
|
763
|
+
return 1
|
764
|
+
|
765
|
+
async def _handle_service_sql_diagnostic(self, args: argparse.Namespace) -> int:
|
766
|
+
"""Handle SQL diagnostic service call command."""
|
767
|
+
try:
|
768
|
+
operation = getattr(args, "operation", "")
|
769
|
+
|
770
|
+
# Prepare parameters based on operation
|
771
|
+
parameters = {}
|
772
|
+
|
773
|
+
if operation == "GetAxSqlResourceStats":
|
774
|
+
since_minutes = getattr(args, "since_minutes", 10)
|
775
|
+
start_time = getattr(args, "start_time", None)
|
776
|
+
end_time = getattr(args, "end_time", None)
|
777
|
+
|
778
|
+
if start_time and end_time:
|
779
|
+
parameters = {
|
780
|
+
"start": start_time,
|
781
|
+
"end": end_time,
|
782
|
+
}
|
783
|
+
else:
|
784
|
+
# Use since_minutes to calculate start/end
|
785
|
+
from datetime import datetime, timezone, timedelta
|
786
|
+
end = datetime.now(timezone.utc)
|
787
|
+
start = end - timedelta(minutes=since_minutes)
|
788
|
+
parameters = {
|
789
|
+
"start": start.isoformat(),
|
790
|
+
"end": end.isoformat(),
|
791
|
+
}
|
792
|
+
|
793
|
+
# Call the SQL diagnostic service
|
794
|
+
response = await self.client.post_json_service(
|
795
|
+
service_group="SysSqlDiagnosticService",
|
796
|
+
service_name="SysSqlDiagnosticServiceOperations",
|
797
|
+
operation_name=operation,
|
798
|
+
parameters=parameters if parameters else None,
|
799
|
+
)
|
800
|
+
|
801
|
+
# Format and display response
|
802
|
+
if response.success:
|
803
|
+
result = {
|
804
|
+
"success": True,
|
805
|
+
"statusCode": response.status_code,
|
806
|
+
"operation": operation,
|
807
|
+
"data": response.data,
|
808
|
+
}
|
809
|
+
|
810
|
+
# Add summary information
|
811
|
+
if isinstance(response.data, list):
|
812
|
+
result["recordCount"] = len(response.data)
|
813
|
+
|
814
|
+
output = self.output_formatter.format_output(result)
|
815
|
+
print(output)
|
816
|
+
return 0
|
817
|
+
else:
|
818
|
+
error_result = {
|
819
|
+
"success": False,
|
820
|
+
"statusCode": response.status_code,
|
821
|
+
"operation": operation,
|
822
|
+
"error": response.error_message,
|
823
|
+
}
|
824
|
+
output = self.output_formatter.format_output(error_result)
|
825
|
+
print(output)
|
826
|
+
return 1
|
827
|
+
|
828
|
+
except Exception as e:
|
829
|
+
print(format_error_message(f"Error calling SQL diagnostic service: {e}"))
|
830
|
+
return 1
|
831
|
+
|
697
832
|
def _handle_error(self, error: Exception, verbose: bool = False) -> None:
|
698
833
|
"""Handle and display errors consistently.
|
699
834
|
|