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.
Files changed (93) hide show
  1. {d365fo_client-0.3.1/src/d365fo_client.egg-info → d365fo_client-0.3.2}/PKG-INFO +114 -3
  2. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/README.md +112 -1
  3. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/pyproject.toml +2 -2
  4. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/cli.py +135 -0
  5. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/client.py +371 -19
  6. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/crud.py +33 -10
  7. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/main.py +58 -0
  8. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/fastmcp_main.py +1 -1
  9. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/fastmcp_utils.py +4 -0
  10. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/base_tools_mixin.py +13 -12
  11. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/crud_tools_mixin.py +181 -40
  12. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/server.py +10 -1
  13. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/__init__.py +2 -0
  14. d365fo_client-0.3.2/src/d365fo_client/mcp/tools/json_service_tools.py +326 -0
  15. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/models.py +45 -0
  16. d365fo_client-0.3.2/src/d365fo_client/odata_serializer.py +300 -0
  17. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/query.py +30 -20
  18. {d365fo_client-0.3.1 → d365fo_client-0.3.2/src/d365fo_client.egg-info}/PKG-INFO +114 -3
  19. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/SOURCES.txt +2 -0
  20. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/requires.txt +1 -1
  21. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/LICENSE +0 -0
  22. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/setup.cfg +0 -0
  23. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/__init__.py +0 -0
  24. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/auth.py +0 -0
  25. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/config.py +0 -0
  26. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/credential_sources.py +0 -0
  27. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/exceptions.py +0 -0
  28. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/labels.py +0 -0
  29. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/__init__.py +0 -0
  30. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/__init__.py +0 -0
  31. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/__init__.py +0 -0
  32. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/auth.py +0 -0
  33. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/oauth_proxy.py +0 -0
  34. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/__init__.py +0 -0
  35. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/apikey.py +0 -0
  36. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/azure.py +0 -0
  37. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/bearer.py +0 -0
  38. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/providers/jwt.py +0 -0
  39. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/auth/redirect_validation.py +0 -0
  40. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/auth_server/dependencies.py +0 -0
  41. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/client_manager.py +0 -0
  42. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/fastmcp_server.py +0 -0
  43. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/main.py +0 -0
  44. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/__init__.py +0 -0
  45. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/connection_tools_mixin.py +0 -0
  46. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/database_tools_mixin.py +0 -0
  47. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/label_tools_mixin.py +0 -0
  48. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/metadata_tools_mixin.py +0 -0
  49. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/performance_tools_mixin.py +0 -0
  50. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/profile_tools_mixin.py +0 -0
  51. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/mixins/sync_tools_mixin.py +0 -0
  52. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/models.py +0 -0
  53. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/prompts/__init__.py +0 -0
  54. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/prompts/action_execution.py +0 -0
  55. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/prompts/sequence_analysis.py +0 -0
  56. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/__init__.py +0 -0
  57. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/database_handler.py +0 -0
  58. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/entity_handler.py +0 -0
  59. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/environment_handler.py +0 -0
  60. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/metadata_handler.py +0 -0
  61. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/resources/query_handler.py +0 -0
  62. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/connection_tools.py +0 -0
  63. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/crud_tools.py +0 -0
  64. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/database_tools.py +0 -0
  65. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/label_tools.py +0 -0
  66. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/metadata_tools.py +0 -0
  67. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/profile_tools.py +0 -0
  68. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/tools/sync_tools.py +0 -0
  69. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/__init__.py +0 -0
  70. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/auth.py +0 -0
  71. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/logging.py +0 -0
  72. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/mcp/utilities/types.py +0 -0
  73. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_api.py +0 -0
  74. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/__init__.py +0 -0
  75. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/cache_v2.py +0 -0
  76. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/database_v2.py +0 -0
  77. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/global_version_manager.py +0 -0
  78. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/label_utils.py +0 -0
  79. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/search_engine_v2.py +0 -0
  80. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/sync_manager_v2.py +0 -0
  81. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/sync_session_manager.py +0 -0
  82. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/metadata_v2/version_detector.py +0 -0
  83. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/output.py +0 -0
  84. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/profile_manager.py +0 -0
  85. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/profiles.py +0 -0
  86. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/session.py +0 -0
  87. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/settings.py +0 -0
  88. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/sync_models.py +0 -0
  89. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client/utils.py +0 -0
  90. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/dependency_links.txt +0 -0
  91. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/entry_points.txt +0 -0
  92. {d365fo_client-0.3.1 → d365fo_client-0.3.2}/src/d365fo_client.egg-info/top_level.txt +0 -0
  93. {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.1
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[standard]>=0.32.0
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
  [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_D365_FO_MCP_Server_(Docker)-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
  [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_D365_FO_MCP_Server_(Docker)-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
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/d365fo-client?label=Downloads)](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
- - 🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
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
  [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_D365_FO_MCP_Server_(Docker)-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
  [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_D365_FO_MCP_Server_(Docker)-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
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/d365fo-client?label=Downloads)](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
- - 🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
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.1"
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[standard]>=0.32.0",
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