d365fo-client 0.2.2__tar.gz → 0.2.3__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 (61) hide show
  1. {d365fo_client-0.2.2/src/d365fo_client.egg-info → d365fo_client-0.2.3}/PKG-INFO +48 -17
  2. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/README.md +46 -16
  3. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/pyproject.toml +2 -1
  4. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/auth.py +48 -9
  5. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/client.py +40 -20
  6. d365fo_client-0.2.3/src/d365fo_client/credential_sources.py +431 -0
  7. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/client_manager.py +8 -0
  8. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/main.py +39 -17
  9. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/server.py +69 -22
  10. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/__init__.py +2 -0
  11. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/profile_tools.py +261 -2
  12. d365fo_client-0.2.3/src/d365fo_client/mcp/tools/sync_tools.py +503 -0
  13. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_api.py +67 -0
  14. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/cache_v2.py +11 -9
  15. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/global_version_manager.py +2 -4
  16. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/sync_manager_v2.py +1 -1
  17. d365fo_client-0.2.3/src/d365fo_client/metadata_v2/sync_session_manager.py +1043 -0
  18. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/models.py +22 -3
  19. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/profile_manager.py +7 -1
  20. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/profiles.py +28 -1
  21. d365fo_client-0.2.3/src/d365fo_client/sync_models.py +181 -0
  22. {d365fo_client-0.2.2 → d365fo_client-0.2.3/src/d365fo_client.egg-info}/PKG-INFO +48 -17
  23. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client.egg-info/SOURCES.txt +4 -0
  24. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client.egg-info/requires.txt +1 -0
  25. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/LICENSE +0 -0
  26. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/setup.cfg +0 -0
  27. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/__init__.py +0 -0
  28. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/cli.py +0 -0
  29. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/config.py +0 -0
  30. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/crud.py +0 -0
  31. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/exceptions.py +0 -0
  32. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/labels.py +0 -0
  33. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/main.py +0 -0
  34. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/__init__.py +0 -0
  35. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/models.py +0 -0
  36. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/prompts/__init__.py +0 -0
  37. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/prompts/action_execution.py +0 -0
  38. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/prompts/sequence_analysis.py +0 -0
  39. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/resources/__init__.py +0 -0
  40. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/resources/database_handler.py +0 -0
  41. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/resources/entity_handler.py +0 -0
  42. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/resources/environment_handler.py +0 -0
  43. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/resources/metadata_handler.py +0 -0
  44. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/resources/query_handler.py +0 -0
  45. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/connection_tools.py +0 -0
  46. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/crud_tools.py +0 -0
  47. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/database_tools.py +0 -0
  48. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/label_tools.py +0 -0
  49. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/mcp/tools/metadata_tools.py +0 -0
  50. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/__init__.py +0 -0
  51. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/database_v2.py +0 -0
  52. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/label_utils.py +0 -0
  53. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/search_engine_v2.py +0 -0
  54. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/metadata_v2/version_detector.py +0 -0
  55. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/output.py +0 -0
  56. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/query.py +0 -0
  57. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/session.py +0 -0
  58. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client/utils.py +0 -0
  59. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client.egg-info/dependency_links.txt +0 -0
  60. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client.egg-info/entry_points.txt +0 -0
  61. {d365fo_client-0.2.2 → d365fo_client-0.2.3}/src/d365fo_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d365fo-client
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Microsoft Dynamics 365 Finance & Operations client
5
5
  Author-email: Muhammad Afzaal <mo@thedataguy.pro>
6
6
  License-Expression: MIT
@@ -22,6 +22,7 @@ License-File: LICENSE
22
22
  Requires-Dist: aiohttp>=3.10.0
23
23
  Requires-Dist: aiofiles>=24.1.0
24
24
  Requires-Dist: azure-identity>=1.19.0
25
+ Requires-Dist: azure-keyvault-secrets>=4.8.0
25
26
  Requires-Dist: requests>=2.32.0
26
27
  Requires-Dist: aiosqlite>=0.19.0
27
28
  Requires-Dist: cachetools>=6.1.0
@@ -50,7 +51,7 @@ A comprehensive Python client library and MCP server for Microsoft Dynamics 365
50
51
  - 🏷️ **Label Operations V2**: Multilingual label caching with performance improvements and async support
51
52
  - 🔍 **Advanced Querying**: Support for all OData query parameters ($select, $filter, $expand, etc.)
52
53
  - ⚡ **Action Execution**: Execute bound and unbound OData actions with comprehensive parameter handling
53
- - 🔒 **Authentication**: Azure AD integration with default credentials and service principal support
54
+ - 🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
54
55
  - 💾 **Intelligent Caching**: Cross-environment cache sharing with module-based version detection
55
56
  - 🌐 **Async/Await**: Modern async/await patterns with optimized session management
56
57
  - 📝 **Type Hints**: Full type annotation support with enhanced data models
@@ -58,6 +59,8 @@ A comprehensive Python client library and MCP server for Microsoft Dynamics 365
58
59
  - 🖥️ **Comprehensive CLI**: Hierarchical command-line interface for all D365 F&O operations
59
60
  - 🧪 **Multi-tier Testing**: Mock, sandbox, and live integration testing framework (17/17 tests passing)
60
61
  - 📋 **Metadata Scripts**: PowerShell and Python utilities for entity, enumeration, and action discovery
62
+ - 🔐 **Enhanced Credential Management**: Support for Azure Key Vault and multiple credential sources
63
+ - 📊 **Advanced Sync Management**: Session-based synchronization with detailed progress tracking
61
64
 
62
65
  ## Installation
63
66
 
@@ -73,6 +76,13 @@ uv sync # Installs with exact dependencies from uv.lock
73
76
 
74
77
  **Note**: The package includes MCP (Model Context Protocol) dependencies by default, enabling AI assistant integration. Both `d365fo-client` CLI and `d365fo-mcp-server` commands will be available after installation.
75
78
 
79
+ **Breaking Change in v0.2.3**: Environment variable names have been updated for consistency:
80
+ - `AZURE_CLIENT_ID` → `D365FO_CLIENT_ID`
81
+ - `AZURE_CLIENT_SECRET` → `D365FO_CLIENT_SECRET`
82
+ - `AZURE_TENANT_ID` → `D365FO_TENANT_ID`
83
+
84
+ Please update your environment variables accordingly when upgrading.
85
+
76
86
  ## Quick Start
77
87
 
78
88
  ## Command Line Interface (CLI)
@@ -158,9 +168,9 @@ profiles:
158
168
 
159
169
  development:
160
170
  base_url: "https://dev.dynamics.com"
161
- client_id: "${AZURE_CLIENT_ID}"
162
- client_secret: "${AZURE_CLIENT_SECRET}"
163
- tenant_id: "${AZURE_TENANT_ID}"
171
+ client_id: "${D365FO_CLIENT_ID}"
172
+ client_secret: "${D365FO_CLIENT_SECRET}"
173
+ tenant_id: "${D365FO_TENANT_ID}"
164
174
  use_cache_first: true
165
175
 
166
176
  default_profile: "development"
@@ -262,7 +272,14 @@ config = FOClientConfig(
262
272
  use_default_credentials=False
263
273
  )
264
274
 
265
- # Option 3: With custom settings
275
+ # Option 3: Azure Key Vault integration (New in v0.2.3)
276
+ config = FOClientConfig(
277
+ base_url="https://your-fo-environment.dynamics.com",
278
+ credential_source="keyvault", # Use Azure Key Vault for credentials
279
+ keyvault_url="https://your-keyvault.vault.azure.net/"
280
+ )
281
+
282
+ # Option 4: With custom settings
266
283
  config = FOClientConfig(
267
284
  base_url="https://your-fo-environment.dynamics.com",
268
285
  use_default_credentials=True,
@@ -462,7 +479,9 @@ d365fo-client/
462
479
  │ ├── metadata.py # Legacy metadata operations
463
480
  │ ├── metadata_api.py # Metadata API client
464
481
  │ ├── metadata_cache.py # Metadata caching layer V2
465
- │ ├── metadata_sync.py # Metadata synchronization V2
482
+ │ ├── metadata_sync.py # Metadata synchronization V2 with session management
483
+ │ ├── sync_session.py # Enhanced sync session management (New in v0.2.3)
484
+ │ ├── credential_manager.py # Credential source management (New in v0.2.3)
466
485
  │ ├── labels.py # Label operations V2
467
486
  │ ├── profiles.py # Profile data models
468
487
  │ ├── profile_manager.py # Profile management
@@ -519,6 +538,8 @@ d365fo-client/
519
538
  | `client_secret` | str | None | Azure AD client secret |
520
539
  | `tenant_id` | str | None | Azure AD tenant ID |
521
540
  | `use_default_credentials` | bool | True | Use Azure Default Credential |
541
+ | `credential_source` | str | "environment" | Credential source: "environment", "keyvault" |
542
+ | `keyvault_url` | str | None | Azure Key Vault URL for credential storage |
522
543
  | `verify_ssl` | bool | False | Verify SSL certificates |
523
544
  | `timeout` | int | 30 | Request timeout in seconds |
524
545
  | `metadata_cache_dir` | str | Platform-specific user cache | Metadata cache directory |
@@ -619,9 +640,9 @@ cp tests/integration/.env.template tests/integration/.env
619
640
  # Edit .env file with your settings:
620
641
  INTEGRATION_TEST_LEVEL=sandbox
621
642
  D365FO_SANDBOX_BASE_URL=https://your-test.dynamics.com
622
- AZURE_CLIENT_ID=your-client-id
623
- AZURE_CLIENT_SECRET=your-client-secret
624
- AZURE_TENANT_ID=your-tenant-id
643
+ D365FO_CLIENT_ID=your-client-id
644
+ D365FO_CLIENT_SECRET=your-client-secret
645
+ D365FO_TENANT_ID=your-tenant-id
625
646
  ```
626
647
 
627
648
  #### Available Commands
@@ -707,9 +728,9 @@ pip install d365fo-client
707
728
 
708
729
  # Set up environment variables
709
730
  export D365FO_BASE_URL="https://your-environment.dynamics.com"
710
- export AZURE_CLIENT_ID="your-client-id" # Optional with default credentials
711
- export AZURE_CLIENT_SECRET="your-client-secret" # Optional with default credentials
712
- export AZURE_TENANT_ID="your-tenant-id" # Optional with default credentials
731
+ export D365FO_CLIENT_ID="your-client-id" # Optional with default credentials
732
+ export D365FO_CLIENT_SECRET="your-client-secret" # Optional with default credentials
733
+ export D365FO_TENANT_ID="your-tenant-id" # Optional with default credentials
713
734
 
714
735
  # Start the MCP server
715
736
  d365fo-mcp-server
@@ -857,9 +878,19 @@ For service principal authentication:
857
878
 
858
879
  ```bash
859
880
  export D365FO_BASE_URL="https://your-environment.dynamics.com"
860
- export AZURE_CLIENT_ID="your-client-id"
861
- export AZURE_CLIENT_SECRET="your-client-secret"
862
- export AZURE_TENANT_ID="your-tenant-id"
881
+ export D365FO_CLIENT_ID="your-client-id"
882
+ export D365FO_CLIENT_SECRET="your-client-secret"
883
+ export D365FO_TENANT_ID="your-tenant-id"
884
+ d365fo-mcp-server
885
+ ```
886
+
887
+ #### Azure Key Vault Integration (New in v0.2.3)
888
+ For secure credential storage using Azure Key Vault:
889
+
890
+ ```bash
891
+ export D365FO_BASE_URL="https://your-environment.dynamics.com"
892
+ export D365FO_CREDENTIAL_SOURCE="keyvault"
893
+ export D365FO_KEYVAULT_URL="https://your-keyvault.vault.azure.net/"
863
894
  d365fo-mcp-server
864
895
  ```
865
896
 
@@ -1032,7 +1063,7 @@ tail -f ~/.d365fo-mcp/logs/mcp-server.log
1032
1063
  az account show
1033
1064
 
1034
1065
  # Test with explicit credentials
1035
- export AZURE_CLIENT_ID="your-client-id"
1066
+ export D365FO_CLIENT_ID="your-client-id"
1036
1067
  # ... set other variables
1037
1068
  d365fo-mcp-server
1038
1069
  ```
@@ -9,7 +9,7 @@ A comprehensive Python client library and MCP server for Microsoft Dynamics 365
9
9
  - 🏷️ **Label Operations V2**: Multilingual label caching with performance improvements and async support
10
10
  - 🔍 **Advanced Querying**: Support for all OData query parameters ($select, $filter, $expand, etc.)
11
11
  - ⚡ **Action Execution**: Execute bound and unbound OData actions with comprehensive parameter handling
12
- - 🔒 **Authentication**: Azure AD integration with default credentials and service principal support
12
+ - 🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
13
13
  - 💾 **Intelligent Caching**: Cross-environment cache sharing with module-based version detection
14
14
  - 🌐 **Async/Await**: Modern async/await patterns with optimized session management
15
15
  - 📝 **Type Hints**: Full type annotation support with enhanced data models
@@ -17,6 +17,8 @@ A comprehensive Python client library and MCP server for Microsoft Dynamics 365
17
17
  - 🖥️ **Comprehensive CLI**: Hierarchical command-line interface for all D365 F&O operations
18
18
  - 🧪 **Multi-tier Testing**: Mock, sandbox, and live integration testing framework (17/17 tests passing)
19
19
  - 📋 **Metadata Scripts**: PowerShell and Python utilities for entity, enumeration, and action discovery
20
+ - 🔐 **Enhanced Credential Management**: Support for Azure Key Vault and multiple credential sources
21
+ - 📊 **Advanced Sync Management**: Session-based synchronization with detailed progress tracking
20
22
 
21
23
  ## Installation
22
24
 
@@ -32,6 +34,13 @@ uv sync # Installs with exact dependencies from uv.lock
32
34
 
33
35
  **Note**: The package includes MCP (Model Context Protocol) dependencies by default, enabling AI assistant integration. Both `d365fo-client` CLI and `d365fo-mcp-server` commands will be available after installation.
34
36
 
37
+ **Breaking Change in v0.2.3**: Environment variable names have been updated for consistency:
38
+ - `AZURE_CLIENT_ID` → `D365FO_CLIENT_ID`
39
+ - `AZURE_CLIENT_SECRET` → `D365FO_CLIENT_SECRET`
40
+ - `AZURE_TENANT_ID` → `D365FO_TENANT_ID`
41
+
42
+ Please update your environment variables accordingly when upgrading.
43
+
35
44
  ## Quick Start
36
45
 
37
46
  ## Command Line Interface (CLI)
@@ -117,9 +126,9 @@ profiles:
117
126
 
118
127
  development:
119
128
  base_url: "https://dev.dynamics.com"
120
- client_id: "${AZURE_CLIENT_ID}"
121
- client_secret: "${AZURE_CLIENT_SECRET}"
122
- tenant_id: "${AZURE_TENANT_ID}"
129
+ client_id: "${D365FO_CLIENT_ID}"
130
+ client_secret: "${D365FO_CLIENT_SECRET}"
131
+ tenant_id: "${D365FO_TENANT_ID}"
123
132
  use_cache_first: true
124
133
 
125
134
  default_profile: "development"
@@ -221,7 +230,14 @@ config = FOClientConfig(
221
230
  use_default_credentials=False
222
231
  )
223
232
 
224
- # Option 3: With custom settings
233
+ # Option 3: Azure Key Vault integration (New in v0.2.3)
234
+ config = FOClientConfig(
235
+ base_url="https://your-fo-environment.dynamics.com",
236
+ credential_source="keyvault", # Use Azure Key Vault for credentials
237
+ keyvault_url="https://your-keyvault.vault.azure.net/"
238
+ )
239
+
240
+ # Option 4: With custom settings
225
241
  config = FOClientConfig(
226
242
  base_url="https://your-fo-environment.dynamics.com",
227
243
  use_default_credentials=True,
@@ -421,7 +437,9 @@ d365fo-client/
421
437
  │ ├── metadata.py # Legacy metadata operations
422
438
  │ ├── metadata_api.py # Metadata API client
423
439
  │ ├── metadata_cache.py # Metadata caching layer V2
424
- │ ├── metadata_sync.py # Metadata synchronization V2
440
+ │ ├── metadata_sync.py # Metadata synchronization V2 with session management
441
+ │ ├── sync_session.py # Enhanced sync session management (New in v0.2.3)
442
+ │ ├── credential_manager.py # Credential source management (New in v0.2.3)
425
443
  │ ├── labels.py # Label operations V2
426
444
  │ ├── profiles.py # Profile data models
427
445
  │ ├── profile_manager.py # Profile management
@@ -478,6 +496,8 @@ d365fo-client/
478
496
  | `client_secret` | str | None | Azure AD client secret |
479
497
  | `tenant_id` | str | None | Azure AD tenant ID |
480
498
  | `use_default_credentials` | bool | True | Use Azure Default Credential |
499
+ | `credential_source` | str | "environment" | Credential source: "environment", "keyvault" |
500
+ | `keyvault_url` | str | None | Azure Key Vault URL for credential storage |
481
501
  | `verify_ssl` | bool | False | Verify SSL certificates |
482
502
  | `timeout` | int | 30 | Request timeout in seconds |
483
503
  | `metadata_cache_dir` | str | Platform-specific user cache | Metadata cache directory |
@@ -578,9 +598,9 @@ cp tests/integration/.env.template tests/integration/.env
578
598
  # Edit .env file with your settings:
579
599
  INTEGRATION_TEST_LEVEL=sandbox
580
600
  D365FO_SANDBOX_BASE_URL=https://your-test.dynamics.com
581
- AZURE_CLIENT_ID=your-client-id
582
- AZURE_CLIENT_SECRET=your-client-secret
583
- AZURE_TENANT_ID=your-tenant-id
601
+ D365FO_CLIENT_ID=your-client-id
602
+ D365FO_CLIENT_SECRET=your-client-secret
603
+ D365FO_TENANT_ID=your-tenant-id
584
604
  ```
585
605
 
586
606
  #### Available Commands
@@ -666,9 +686,9 @@ pip install d365fo-client
666
686
 
667
687
  # Set up environment variables
668
688
  export D365FO_BASE_URL="https://your-environment.dynamics.com"
669
- export AZURE_CLIENT_ID="your-client-id" # Optional with default credentials
670
- export AZURE_CLIENT_SECRET="your-client-secret" # Optional with default credentials
671
- export AZURE_TENANT_ID="your-tenant-id" # Optional with default credentials
689
+ export D365FO_CLIENT_ID="your-client-id" # Optional with default credentials
690
+ export D365FO_CLIENT_SECRET="your-client-secret" # Optional with default credentials
691
+ export D365FO_TENANT_ID="your-tenant-id" # Optional with default credentials
672
692
 
673
693
  # Start the MCP server
674
694
  d365fo-mcp-server
@@ -816,9 +836,19 @@ For service principal authentication:
816
836
 
817
837
  ```bash
818
838
  export D365FO_BASE_URL="https://your-environment.dynamics.com"
819
- export AZURE_CLIENT_ID="your-client-id"
820
- export AZURE_CLIENT_SECRET="your-client-secret"
821
- export AZURE_TENANT_ID="your-tenant-id"
839
+ export D365FO_CLIENT_ID="your-client-id"
840
+ export D365FO_CLIENT_SECRET="your-client-secret"
841
+ export D365FO_TENANT_ID="your-tenant-id"
842
+ d365fo-mcp-server
843
+ ```
844
+
845
+ #### Azure Key Vault Integration (New in v0.2.3)
846
+ For secure credential storage using Azure Key Vault:
847
+
848
+ ```bash
849
+ export D365FO_BASE_URL="https://your-environment.dynamics.com"
850
+ export D365FO_CREDENTIAL_SOURCE="keyvault"
851
+ export D365FO_KEYVAULT_URL="https://your-keyvault.vault.azure.net/"
822
852
  d365fo-mcp-server
823
853
  ```
824
854
 
@@ -991,7 +1021,7 @@ tail -f ~/.d365fo-mcp/logs/mcp-server.log
991
1021
  az account show
992
1022
 
993
1023
  # Test with explicit credentials
994
- export AZURE_CLIENT_ID="your-client-id"
1024
+ export D365FO_CLIENT_ID="your-client-id"
995
1025
  # ... set other variables
996
1026
  d365fo-mcp-server
997
1027
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "d365fo-client"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Microsoft Dynamics 365 Finance & Operations client"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "aiohttp>=3.10.0",
11
11
  "aiofiles>=24.1.0",
12
12
  "azure-identity>=1.19.0",
13
+ "azure-keyvault-secrets>=4.8.0",
13
14
  "requests>=2.32.0",
14
15
  "aiosqlite>=0.19.0",
15
16
  "cachetools>=6.1.0",
@@ -5,6 +5,7 @@ from typing import Optional
5
5
 
6
6
  from azure.identity import ClientSecretCredential, DefaultAzureCredential
7
7
 
8
+ from .credential_sources import CredentialManager, CredentialSource
8
9
  from .models import FOClientConfig
9
10
 
10
11
 
@@ -20,25 +21,46 @@ class AuthenticationManager:
20
21
  self.config = config
21
22
  self._token = None
22
23
  self._token_expires = None
23
- self.credential = self._setup_credentials()
24
-
25
- def _setup_credentials(self):
26
- """Setup authentication credentials"""
27
- if self.config.use_default_credentials:
28
- return DefaultAzureCredential()
29
- elif (
24
+ self._credential_manager = CredentialManager()
25
+ self.credential = None # Will be set by _setup_credentials
26
+
27
+ async def _setup_credentials(self):
28
+ """Setup authentication credentials with support for credential sources"""
29
+
30
+ # Check if credential source is specified in config
31
+ credential_source = getattr(self.config, 'credential_source', None)
32
+
33
+ if credential_source is not None:
34
+ # Use credential source to get credentials
35
+ try:
36
+ client_id, client_secret, tenant_id = await self._credential_manager.get_credentials(credential_source)
37
+ self.credential = ClientSecretCredential(
38
+ tenant_id=tenant_id,
39
+ client_id=client_id,
40
+ client_secret=client_secret,
41
+ )
42
+ return
43
+ except Exception as e:
44
+ raise ValueError(f"Failed to setup credentials from source: {e}")
45
+
46
+
47
+ # Fallback to existing logic for backward compatibility
48
+
49
+ if (
30
50
  self.config.client_id
31
51
  and self.config.client_secret
32
52
  and self.config.tenant_id
33
53
  ):
34
- return ClientSecretCredential(
54
+ self.credential = ClientSecretCredential(
35
55
  tenant_id=self.config.tenant_id,
36
56
  client_id=self.config.client_id,
37
57
  client_secret=self.config.client_secret,
38
58
  )
59
+ elif self.config.use_default_credentials:
60
+ self.credential = DefaultAzureCredential()
39
61
  else:
40
62
  raise ValueError(
41
- "Must provide either use_default_credentials=True or client credentials"
63
+ "Must provide either use_default_credentials=True, client credentials, or credential_source"
42
64
  )
43
65
 
44
66
  async def get_token(self) -> str:
@@ -51,6 +73,10 @@ class AuthenticationManager:
51
73
  if self._is_localhost():
52
74
  return "mock-token-for-localhost"
53
75
 
76
+ # Initialize credentials if not already set
77
+ if self.credential is None:
78
+ await self._setup_credentials()
79
+
54
80
  if (
55
81
  self._token
56
82
  and self._token_expires
@@ -91,3 +117,16 @@ class AuthenticationManager:
91
117
  """Invalidate cached token to force refresh"""
92
118
  self._token = None
93
119
  self._token_expires = None
120
+
121
+ async def invalidate_credentials(self):
122
+ """Invalidate cached credentials and token to force full refresh"""
123
+ self.invalidate_token()
124
+ self.credential = None
125
+ if hasattr(self, '_credential_manager'):
126
+ self._credential_manager.clear_cache()
127
+
128
+ def get_credential_cache_stats(self) -> dict:
129
+ """Get credential cache statistics for debugging"""
130
+ if hasattr(self, '_credential_manager'):
131
+ return self._credential_manager.get_cache_stats()
132
+ return {"total_cached": 0, "expired": 0, "active": 0}
@@ -11,6 +11,7 @@ from .exceptions import FOClientError
11
11
  from .labels import LabelOperations, resolve_labels_generic
12
12
  from .metadata_api import MetadataAPIOperations
13
13
  from .metadata_v2 import MetadataCacheV2, SmartSyncManagerV2
14
+ from .metadata_v2.sync_session_manager import SyncSessionManager
14
15
  from .models import (
15
16
  ActionInfo,
16
17
  DataEntityInfo,
@@ -58,6 +59,7 @@ class FOClient:
58
59
  # Initialize new metadata cache and sync components
59
60
  self.metadata_cache = None
60
61
  self.sync_manager = None
62
+ self._sync_session_manager = None
61
63
  self._metadata_initialized = False
62
64
  self._background_sync_task = None
63
65
 
@@ -83,6 +85,10 @@ class FOClient:
83
85
 
84
86
  await self.session_manager.close()
85
87
 
88
+
89
+ async def initialize_metadata(self):
90
+ await self._ensure_metadata_initialized()
91
+
86
92
  async def _ensure_metadata_initialized(self):
87
93
  """Ensure metadata cache and sync manager are initialized"""
88
94
  if not self._metadata_initialized and self.config.enable_metadata_cache:
@@ -104,6 +110,9 @@ class FOClient:
104
110
  self.metadata_cache, self.metadata_api_ops
105
111
  )
106
112
 
113
+ # Initialize sync message with session
114
+ self._sync_session_manager = SyncSessionManager(self.metadata_cache, self.metadata_api_ops)
115
+
107
116
  self._metadata_initialized = True
108
117
  self.logger.debug("Metadata cache v2 with label caching initialized")
109
118
 
@@ -144,18 +153,9 @@ class FOClient:
144
153
  f"Starting background metadata sync for version {global_version_id}"
145
154
  )
146
155
 
147
- # Use self as the fo_client for sync - SmartSyncManagerV2 expects a client with metadata API operations
148
- result = await self.sync_manager.sync_metadata(global_version_id)
149
-
150
- if result.success:
151
- self.logger.info(
152
- f"Background sync completed: "
153
- f"{result.entity_count} entities, "
154
- f"{result.enumeration_count} enumerations, "
155
- f"{result.duration_ms:.2f}ms"
156
- )
157
- else:
158
- self.logger.warning(f"Background sync failed: {result.error}")
156
+
157
+ self.sync_session_manager.start_sync_session(global_version_id=global_version_id,initiated_by="background_task")
158
+
159
159
 
160
160
  except Exception as e:
161
161
  self.logger.error(f"Background sync error: {e}")
@@ -172,6 +172,27 @@ class FOClient:
172
172
  and not self._background_sync_task.done()
173
173
  )
174
174
 
175
+ @property
176
+ def sync_session_manager(self) -> SyncSessionManager:
177
+ """Get sync session manager (lazy initialization).
178
+
179
+ Returns:
180
+ SyncSessionManager instance for enhanced sync progress tracking
181
+
182
+ Raises:
183
+ RuntimeError: If metadata cache is not initialized
184
+ """
185
+ if self._sync_session_manager is None:
186
+ if not self.metadata_cache:
187
+ raise RuntimeError("Metadata cache must be initialized before accessing sync session manager")
188
+
189
+ self._sync_session_manager = SyncSessionManager(
190
+ cache=self.metadata_cache,
191
+ metadata_api=self.metadata_api_ops
192
+ )
193
+
194
+ return self._sync_session_manager
195
+
175
196
  async def _get_from_cache_first(
176
197
  self,
177
198
  cache_method,
@@ -687,17 +708,16 @@ class FOClient:
687
708
 
688
709
  async def get_data_entities(
689
710
  self, options: Optional[QueryOptions] = None
690
- ) -> List[DataEntityInfo]:
691
- """Get data entities - updated to return list for v2 sync compatibility
711
+ ) -> Dict[str, Any]:
712
+ """Get data entities from DataEntities metadata endpoint
692
713
 
693
714
  Args:
694
- options: OData query options (ignored for now)
715
+ options: OData query options
695
716
 
696
717
  Returns:
697
- List of DataEntityInfo objects
718
+ Response containing data entities
698
719
  """
699
- # For sync manager compatibility, return list of DataEntityInfo objects
700
- return await self.metadata_api_ops.search_data_entities("") # Get all entities
720
+ return await self.metadata_api_ops.get_data_entities(options)
701
721
 
702
722
  async def get_data_entities_raw(
703
723
  self, options: Optional[QueryOptions] = None
@@ -934,7 +954,7 @@ class FOClient:
934
954
  Returns:
935
955
  Response containing public enumerations
936
956
  """
937
- self._ensure_metadata_initialized()
957
+ await self._ensure_metadata_initialized()
938
958
 
939
959
  return await self.metadata_api_ops.get_public_enumerations(options)
940
960
 
@@ -1188,7 +1208,7 @@ class FOClient:
1188
1208
  "advanced_cache_enabled": True,
1189
1209
  "cache_v2_enabled": True,
1190
1210
  "cache_initialized": self._metadata_initialized,
1191
- "sync_manager_available": self.sync_manager is not None,
1211
+ "sync_manager_available": self.sync_manager is not None or self._sync_session_manager is not None,
1192
1212
  "background_sync_running": self._is_background_sync_running(),
1193
1213
  "statistics": stats,
1194
1214
  }