d365fo-client 0.2.4__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. d365fo_client/__init__.py +7 -1
  2. d365fo_client/auth.py +9 -21
  3. d365fo_client/cli.py +25 -13
  4. d365fo_client/client.py +8 -4
  5. d365fo_client/config.py +52 -30
  6. d365fo_client/credential_sources.py +5 -0
  7. d365fo_client/main.py +1 -1
  8. d365fo_client/mcp/__init__.py +3 -1
  9. d365fo_client/mcp/auth_server/__init__.py +5 -0
  10. d365fo_client/mcp/auth_server/auth/__init__.py +30 -0
  11. d365fo_client/mcp/auth_server/auth/auth.py +372 -0
  12. d365fo_client/mcp/auth_server/auth/oauth_proxy.py +989 -0
  13. d365fo_client/mcp/auth_server/auth/providers/__init__.py +0 -0
  14. d365fo_client/mcp/auth_server/auth/providers/azure.py +325 -0
  15. d365fo_client/mcp/auth_server/auth/providers/bearer.py +25 -0
  16. d365fo_client/mcp/auth_server/auth/providers/jwt.py +547 -0
  17. d365fo_client/mcp/auth_server/auth/redirect_validation.py +65 -0
  18. d365fo_client/mcp/auth_server/dependencies.py +136 -0
  19. d365fo_client/mcp/client_manager.py +16 -67
  20. d365fo_client/mcp/fastmcp_main.py +358 -0
  21. d365fo_client/mcp/fastmcp_server.py +598 -0
  22. d365fo_client/mcp/fastmcp_utils.py +431 -0
  23. d365fo_client/mcp/main.py +40 -13
  24. d365fo_client/mcp/mixins/__init__.py +24 -0
  25. d365fo_client/mcp/mixins/base_tools_mixin.py +55 -0
  26. d365fo_client/mcp/mixins/connection_tools_mixin.py +50 -0
  27. d365fo_client/mcp/mixins/crud_tools_mixin.py +311 -0
  28. d365fo_client/mcp/mixins/database_tools_mixin.py +685 -0
  29. d365fo_client/mcp/mixins/label_tools_mixin.py +87 -0
  30. d365fo_client/mcp/mixins/metadata_tools_mixin.py +565 -0
  31. d365fo_client/mcp/mixins/performance_tools_mixin.py +109 -0
  32. d365fo_client/mcp/mixins/profile_tools_mixin.py +713 -0
  33. d365fo_client/mcp/mixins/sync_tools_mixin.py +321 -0
  34. d365fo_client/mcp/prompts/action_execution.py +1 -1
  35. d365fo_client/mcp/prompts/sequence_analysis.py +1 -1
  36. d365fo_client/mcp/tools/crud_tools.py +3 -3
  37. d365fo_client/mcp/tools/sync_tools.py +1 -1
  38. d365fo_client/mcp/utilities/__init__.py +1 -0
  39. d365fo_client/mcp/utilities/auth.py +34 -0
  40. d365fo_client/mcp/utilities/logging.py +58 -0
  41. d365fo_client/mcp/utilities/types.py +426 -0
  42. d365fo_client/metadata_v2/sync_manager_v2.py +2 -0
  43. d365fo_client/metadata_v2/sync_session_manager.py +7 -7
  44. d365fo_client/models.py +139 -139
  45. d365fo_client/output.py +2 -2
  46. d365fo_client/profile_manager.py +62 -27
  47. d365fo_client/profiles.py +118 -113
  48. d365fo_client/settings.py +355 -0
  49. d365fo_client/sync_models.py +85 -2
  50. d365fo_client/utils.py +2 -1
  51. {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/METADATA +273 -18
  52. d365fo_client-0.3.0.dist-info/RECORD +84 -0
  53. d365fo_client-0.3.0.dist-info/entry_points.txt +4 -0
  54. d365fo_client-0.2.4.dist-info/RECORD +0 -56
  55. d365fo_client-0.2.4.dist-info/entry_points.txt +0 -3
  56. {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/WHEEL +0 -0
  57. {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
  58. {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,325 @@
1
+ """Azure (Microsoft Entra) OAuth provider for FastMCP.
2
+
3
+ This provider implements Azure/Microsoft Entra ID OAuth authentication
4
+ using the OAuth Proxy pattern for non-DCR OAuth flows.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from unittest import result
12
+
13
+ import httpx
14
+ from mcp.server.auth.provider import AuthorizationParams
15
+ from mcp.shared.auth import OAuthClientInformationFull
16
+ from pydantic import SecretStr, field_validator
17
+ from pydantic_settings import BaseSettings, SettingsConfigDict
18
+
19
+ from ..auth import AccessToken, TokenVerifier
20
+ from ..oauth_proxy import OAuthProxy
21
+ from d365fo_client.mcp.utilities.auth import parse_scopes
22
+ from d365fo_client.mcp.utilities.logging import get_logger
23
+ from d365fo_client.mcp.utilities.types import NotSet, NotSetT
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class AzureProviderSettings(BaseSettings):
29
+ """Settings for Azure OAuth provider."""
30
+
31
+ model_config = SettingsConfigDict(
32
+ env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
33
+ env_file=".env",
34
+ extra="ignore",
35
+ )
36
+
37
+ client_id: str | None = None
38
+ client_secret: SecretStr | None = None
39
+ tenant_id: str | None = None
40
+ base_url: str | None = None
41
+ redirect_path: str | None = None
42
+ required_scopes: list[str] | None = None
43
+ timeout_seconds: int | None = None
44
+ allowed_client_redirect_uris: list[str] | None = None
45
+ clients_storage_path: str | None = None
46
+
47
+ @field_validator("required_scopes", mode="before")
48
+ @classmethod
49
+ def _parse_scopes(cls, v):
50
+ return parse_scopes(v)
51
+
52
+
53
+ class AzureTokenVerifier(TokenVerifier):
54
+ """Token verifier for Azure OAuth tokens.
55
+
56
+ Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
57
+ to get user information and validate the token.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ required_scopes: list[str] | None = None,
64
+ timeout_seconds: int = 10,
65
+ ):
66
+ """Initialize the Azure token verifier.
67
+
68
+ Args:
69
+ required_scopes: Required OAuth scopes
70
+ timeout_seconds: HTTP request timeout
71
+ """
72
+ super().__init__(required_scopes=required_scopes)
73
+ self.timeout_seconds = timeout_seconds
74
+
75
+ async def verify_token(self, token: str) -> AccessToken | None:
76
+ """Verify Azure OAuth token by calling Microsoft Graph API."""
77
+ try:
78
+ async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
79
+ # Use Microsoft Graph API to validate token and get user info
80
+ response = await client.get(
81
+ "https://graph.microsoft.com/v1.0/me",
82
+ headers={
83
+ "Authorization": f"Bearer {token}",
84
+ "User-Agent": "FastMCP-Azure-OAuth",
85
+ },
86
+ )
87
+
88
+ if response.status_code != 200:
89
+ logger.debug(
90
+ "Azure token verification failed: %d - %s",
91
+ response.status_code,
92
+ response.text[:200],
93
+ )
94
+ return None
95
+
96
+ user_data = response.json()
97
+
98
+ # Create AccessToken with Azure user info
99
+ return AccessToken(
100
+ token=token,
101
+ client_id=str(user_data.get("id", "unknown")),
102
+ scopes=self.required_scopes or [],
103
+ expires_at=None,
104
+ claims={
105
+ "sub": user_data.get("id"),
106
+ "email": user_data.get("mail")
107
+ or user_data.get("userPrincipalName"),
108
+ "name": user_data.get("displayName"),
109
+ "given_name": user_data.get("givenName"),
110
+ "family_name": user_data.get("surname"),
111
+ "job_title": user_data.get("jobTitle"),
112
+ "office_location": user_data.get("officeLocation"),
113
+ },
114
+ )
115
+
116
+ except httpx.RequestError as e:
117
+ logger.debug("Failed to verify Azure token: %s", e)
118
+ return None
119
+ except Exception as e:
120
+ logger.debug("Azure token verification error: %s", e)
121
+ return None
122
+
123
+
124
+ class AzureProvider(OAuthProxy):
125
+ """Azure (Microsoft Entra) OAuth provider for FastMCP.
126
+
127
+ This provider implements Azure/Microsoft Entra ID authentication using the
128
+ OAuth Proxy pattern. It supports both organizational accounts and personal
129
+ Microsoft accounts depending on the tenant configuration.
130
+
131
+ Features:
132
+ - Transparent OAuth proxy to Azure/Microsoft identity platform
133
+ - Automatic token validation via Microsoft Graph API
134
+ - User information extraction
135
+ - Support for different tenant configurations (common, organizations, consumers)
136
+
137
+ Setup Requirements:
138
+ 1. Register an application in Azure Portal (portal.azure.com)
139
+ 2. Configure redirect URI as: http://localhost:8000/auth/callback
140
+ 3. Note your Application (client) ID and create a client secret
141
+ 4. Optionally note your Directory (tenant) ID for single-tenant apps
142
+
143
+ Example:
144
+ ```python
145
+ from fastmcp import FastMCP
146
+ from fastmcp.server.auth.providers.azure import AzureProvider
147
+
148
+ auth = AzureProvider(
149
+ client_id="your-client-id",
150
+ client_secret="your-client-secret",
151
+ tenant_id="your-tenant-id", # Required: your Azure tenant ID from Azure Portal
152
+ base_url="http://localhost:8000"
153
+ )
154
+
155
+ mcp = FastMCP("My App", auth=auth)
156
+ ```
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ *,
162
+ client_id: str | NotSetT = NotSet,
163
+ client_secret: str | NotSetT = NotSet,
164
+ tenant_id: str | NotSetT = NotSet,
165
+ base_url: str | NotSetT = NotSet,
166
+ redirect_path: str | NotSetT = NotSet,
167
+ required_scopes: list[str] | None | NotSetT = NotSet,
168
+ timeout_seconds: int | NotSetT = NotSet,
169
+ allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
170
+ clients_storage_path: str | NotSetT = NotSet, # Path to store clients data
171
+ ):
172
+ """Initialize Azure OAuth provider.
173
+
174
+ Args:
175
+ client_id: Azure application (client) ID
176
+ client_secret: Azure client secret
177
+ tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
178
+ base_url: Public URL of your FastMCP server (for OAuth callbacks)
179
+ redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
180
+ required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
181
+ timeout_seconds: HTTP request timeout for Azure API calls
182
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
183
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
184
+ """
185
+ settings = AzureProviderSettings.model_validate(
186
+ {
187
+ k: v
188
+ for k, v in {
189
+ "client_id": client_id,
190
+ "client_secret": client_secret,
191
+ "tenant_id": tenant_id,
192
+ "base_url": base_url,
193
+ "redirect_path": redirect_path,
194
+ "required_scopes": required_scopes,
195
+ "timeout_seconds": timeout_seconds,
196
+ "allowed_client_redirect_uris": allowed_client_redirect_uris,
197
+ "clients_storage_path": clients_storage_path,
198
+ }.items()
199
+ if v is not NotSet
200
+ }
201
+ )
202
+
203
+ # Validate required settings
204
+ if not settings.client_id:
205
+ raise ValueError(
206
+ "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
207
+ )
208
+ if not settings.client_secret:
209
+ raise ValueError(
210
+ "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
211
+ )
212
+
213
+ # Validate tenant_id is provided
214
+ if not settings.tenant_id:
215
+ raise ValueError(
216
+ "tenant_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. "
217
+ "Use your Azure tenant ID (found in Azure Portal), 'organizations', or 'consumers'"
218
+ )
219
+
220
+ # Apply defaults
221
+ tenant_id_final = settings.tenant_id
222
+
223
+ redirect_path_final = settings.redirect_path or "/auth/callback"
224
+ timeout_seconds_final = settings.timeout_seconds or 10
225
+ # Default scopes for Azure - User.Read gives us access to user info via Graph API
226
+ scopes_final = settings.required_scopes or [
227
+ "User.Read",
228
+ "email",
229
+ "openid",
230
+ "profile",
231
+ ]
232
+ allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
233
+
234
+ # Extract secret string from SecretStr
235
+ client_secret_str = (
236
+ settings.client_secret.get_secret_value() if settings.client_secret else ""
237
+ )
238
+
239
+ # Create Azure token verifier
240
+ token_verifier = AzureTokenVerifier(
241
+ required_scopes=scopes_final,
242
+ timeout_seconds=timeout_seconds_final,
243
+ )
244
+
245
+ # Build Azure OAuth endpoints with tenant
246
+ authorization_endpoint = (
247
+ f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
248
+ )
249
+ token_endpoint = (
250
+ f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/token"
251
+ )
252
+
253
+ # Initialize OAuth proxy with Azure endpoints
254
+ super().__init__(
255
+ upstream_authorization_endpoint=authorization_endpoint,
256
+ upstream_token_endpoint=token_endpoint,
257
+ upstream_client_id=settings.client_id,
258
+ upstream_client_secret=client_secret_str,
259
+ token_verifier=token_verifier,
260
+ base_url=settings.base_url, #type: ignore[arg-type]
261
+ redirect_path=redirect_path_final,
262
+ issuer_url=settings.base_url,
263
+ allowed_client_redirect_uris=allowed_client_redirect_uris_final,
264
+ )
265
+
266
+ self.clients_storage_path = settings.clients_storage_path
267
+
268
+ self._load_clients()
269
+
270
+ logger.info(
271
+ "Initialized Azure OAuth provider for client %s with tenant %s",
272
+ settings.client_id,
273
+ tenant_id_final,
274
+ )
275
+
276
+ async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
277
+ """Authorize request, removing 'resource' parameter if present."""
278
+
279
+ if params.resource:
280
+ params.resource = None # Azure does not use 'resource' parameter
281
+
282
+ return await super().authorize(client, params)
283
+
284
+
285
+ async def register_client(self, client_info: OAuthClientInformationFull) -> None:
286
+ """Register a new MCP client, validating redirect URIs if configured."""
287
+ result = await super().register_client(client_info)
288
+ self._save_clients()
289
+ return result
290
+
291
+ def _save_clients(self) -> str | None:
292
+ if not self.clients_storage_path:
293
+ logger.warning("No clients storage path configured. Skipping client save.")
294
+ return None
295
+
296
+ # Store self._clients to clients.json
297
+ try:
298
+ client_json_path = Path(self.clients_storage_path) / "clients.json"
299
+ # Convert OAuthClientInformationFull objects to dictionaries for JSON serialization
300
+ clients_dict = {
301
+ client_id: client.model_dump() if hasattr(client, 'model_dump') else client.__dict__
302
+ for client_id, client in self._clients.items()
303
+ }
304
+ with client_json_path.open("w") as f:
305
+ json.dump(clients_dict, f, indent=2)
306
+ except Exception as e:
307
+ logger.error(f"Failed to write client data to {client_json_path}: {e}")
308
+
309
+ def _load_clients(self) -> None:
310
+ if not self.clients_storage_path:
311
+ return
312
+
313
+ # Load existing clients from storage if path is provided
314
+
315
+ try:
316
+ client_json_path = Path(self.clients_storage_path) / "clients.json"
317
+ if client_json_path.exists():
318
+ with client_json_path.open("r") as f:
319
+ clients_data = json.load(f)
320
+ for client_id, client_info in clients_data.items():
321
+ # Ensure client_id is a string (it should be from JSON, but type checking requires this)
322
+ if isinstance(client_id, str):
323
+ self._clients[client_id] = OAuthClientInformationFull.model_validate(client_info)
324
+ except Exception as e:
325
+ logger.error(f"Failed to load clients from {client_json_path}: {e}")
@@ -0,0 +1,25 @@
1
+ """Backwards compatibility shim for BearerAuthProvider.
2
+
3
+ The BearerAuthProvider class has been moved to fastmcp.server.auth.providers.jwt.JWTVerifier
4
+ for better organization. This module provides a backwards-compatible import.
5
+ """
6
+
7
+ import warnings
8
+
9
+
10
+ from ..providers.jwt import JWKData, JWKSData, RSAKeyPair
11
+ from ..providers.jwt import JWTVerifier as BearerAuthProvider
12
+
13
+ # Re-export for backwards compatibility
14
+ __all__ = ["BearerAuthProvider", "RSAKeyPair", "JWKData", "JWKSData"]
15
+
16
+ # Deprecated in 2.11
17
+ # if fastmcp.settings.deprecation_warnings:
18
+ # warnings.warn(
19
+ # "The `fastmcp.server.auth.providers.bearer` module is deprecated "
20
+ # "and will be removed in a future version. "
21
+ # "Please use `fastmcp.server.auth.providers.jwt.JWTVerifier` "
22
+ # "instead of this module's BearerAuthProvider.",
23
+ # DeprecationWarning,
24
+ # stacklevel=2,
25
+ # )