d365fo-client 0.3.0__py3-none-any.whl → 0.3.1__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.
@@ -0,0 +1,83 @@
1
+ """API Key authentication provider for FastMCP.
2
+
3
+ This provider implements simple API key authentication using the Authorization header.
4
+ Suitable for service-to-service authentication and simpler deployment scenarios.
5
+
6
+ IMPORTANT: FastMCP uses BearerAuthBackend which extracts tokens from the Authorization header
7
+ and calls token_verifier.verify_token(). Clients must send the API key as:
8
+ Authorization: Bearer <your-api-key>
9
+
10
+ The token_verifier.verify_token() method performs constant-time comparison of the API key.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import secrets
16
+
17
+ from pydantic import SecretStr
18
+
19
+ from ..auth import AccessToken, TokenVerifier
20
+ from d365fo_client.mcp.utilities.logging import get_logger
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class APIKeyVerifier(TokenVerifier):
26
+ """API Key token verifier for FastMCP.
27
+
28
+ This is a TokenVerifier that validates API keys sent as Bearer tokens.
29
+ FastMCP's BearerAuthBackend extracts the token from "Authorization: Bearer <token>"
30
+ and passes it to this verifier's verify_token() method.
31
+
32
+ This is a simpler alternative to OAuth for scenarios where:
33
+ - Service-to-service authentication is needed
34
+ - Simplified deployment without OAuth infrastructure
35
+ - Single-user or trusted client scenarios
36
+
37
+ Security features:
38
+ - Constant-time comparison to prevent timing attacks
39
+ - SecretStr storage to prevent accidental logging
40
+ - No token expiration (suitable for long-lived API keys)
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: SecretStr,
46
+ base_url: str | None = None,
47
+ required_scopes: list[str] | None = None,
48
+ ):
49
+ """Initialize API key provider.
50
+
51
+ Args:
52
+ api_key: The secret API key value
53
+ base_url: Base URL of the server
54
+ required_scopes: Required scopes (for compatibility, not enforced for API keys)
55
+ """
56
+ super().__init__(base_url=base_url, required_scopes=required_scopes)
57
+ self.api_key = api_key
58
+
59
+ async def verify_token(self, token: str) -> AccessToken | None:
60
+ """Verify API key token.
61
+
62
+ This method is called by FastMCP's BearerAuthBackend after extracting
63
+ the token from "Authorization: Bearer <token>" header.
64
+
65
+ Args:
66
+ token: The API key extracted from the Authorization header
67
+
68
+ Returns:
69
+ AccessToken if valid, None otherwise
70
+ """
71
+ # Constant-time comparison to prevent timing attacks
72
+ if secrets.compare_digest(token, self.api_key.get_secret_value()):
73
+ logger.debug("API key authentication successful")
74
+ return AccessToken(
75
+ token=token,
76
+ scopes=self.required_scopes or [],
77
+ client_id="api_key_client", # Fixed client_id for API key auth
78
+ expires_at=None, # API keys don't expire
79
+ resource=None,
80
+ )
81
+
82
+ logger.warning("Invalid API key provided")
83
+ return None
@@ -284,42 +284,110 @@ class AzureProvider(OAuthProxy):
284
284
 
285
285
  async def register_client(self, client_info: OAuthClientInformationFull) -> None:
286
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
287
+ await super().register_client(client_info)
288
+ try:
289
+ self._save_clients()
290
+ except Exception as e:
291
+ logger.error(f"Failed to persist client registration: {e}")
292
+ # Don't raise here as the client is already registered in memory
290
293
 
291
- def _save_clients(self) -> str | None:
294
+ def _save_clients(self) -> None:
295
+ """Save client data to persistent storage.
296
+
297
+ Raises:
298
+ ValueError: If clients_storage_path is not configured
299
+ OSError: If file operations fail
300
+ """
292
301
  if not self.clients_storage_path:
293
302
  logger.warning("No clients storage path configured. Skipping client save.")
294
- return None
303
+ return
295
304
 
296
- # Store self._clients to clients.json
297
305
  try:
298
- client_json_path = Path(self.clients_storage_path) / "clients.json"
306
+ # Ensure the storage directory exists
307
+ storage_dir = Path(self.clients_storage_path)
308
+ storage_dir.mkdir(parents=True, exist_ok=True)
309
+
310
+ client_json_path = storage_dir / "clients.json"
311
+
299
312
  # 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)
313
+ # Use mode="json" to properly serialize complex types like AnyUrl
314
+ clients_dict = {}
315
+ for client_id, client in self._clients.items():
316
+ try:
317
+ if hasattr(client, 'model_dump'):
318
+ # Use json mode to ensure proper serialization of complex types (e.g., AnyUrl)
319
+ clients_dict[client_id] = client.model_dump(mode="json")
320
+ else:
321
+ # Fallback for non-Pydantic objects (shouldn't happen with OAuthClientInformationFull)
322
+ clients_dict[client_id] = client.__dict__
323
+ except Exception as client_error:
324
+ logger.error(f"Failed to serialize client {client_id}: {client_error}")
325
+ continue
326
+
327
+ # Write to temporary file first, then rename for atomic operation
328
+ temp_path = client_json_path.with_suffix('.tmp')
329
+ with temp_path.open("w") as f:
330
+ json.dump(clients_dict, f, indent=2, ensure_ascii=False)
331
+
332
+ # Atomic rename
333
+ temp_path.replace(client_json_path)
334
+
335
+ logger.debug(f"Successfully saved {len(clients_dict)} clients to {client_json_path}")
336
+
306
337
  except Exception as e:
307
- logger.error(f"Failed to write client data to {client_json_path}: {e}")
338
+ logger.error(f"Failed to save client data to {self.clients_storage_path}: {e}")
339
+ raise
308
340
 
309
341
  def _load_clients(self) -> None:
342
+ """Load client data from persistent storage.
343
+
344
+ Loads clients from the JSON file if it exists and is valid.
345
+ Invalid client data is logged and skipped.
346
+ """
310
347
  if not self.clients_storage_path:
348
+ logger.debug("No clients storage path configured. Skipping client load.")
311
349
  return
312
350
 
313
- # Load existing clients from storage if path is provided
314
-
315
351
  try:
316
352
  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)
353
+
354
+ if not client_json_path.exists():
355
+ logger.debug(f"Client storage file {client_json_path} does not exist. Starting with empty client registry.")
356
+ return
357
+
358
+ # Read and parse the JSON file
359
+ with client_json_path.open("r", encoding="utf-8") as f:
360
+ clients_data = json.load(f)
361
+
362
+ if not isinstance(clients_data, dict):
363
+ logger.error(f"Invalid client data format in {client_json_path}: expected dict, got {type(clients_data)}")
364
+ return
365
+
366
+ loaded_count = 0
367
+ for client_id, client_info in clients_data.items():
368
+ try:
369
+ # Validate client_id is a string
370
+ if not isinstance(client_id, str):
371
+ logger.warning(f"Skipping client with non-string ID: {client_id} (type: {type(client_id)})")
372
+ continue
373
+
374
+ # Validate and restore the client object
375
+ if not isinstance(client_info, dict):
376
+ logger.warning(f"Skipping client {client_id}: invalid data format (expected dict, got {type(client_info)})")
377
+ continue
378
+
379
+ # Use Pydantic model_validate to restore the object with proper validation
380
+ client_obj = OAuthClientInformationFull.model_validate(client_info)
381
+ self._clients[client_id] = client_obj
382
+ loaded_count += 1
383
+
384
+ except Exception as client_error:
385
+ logger.error(f"Failed to load client {client_id}: {client_error}")
386
+ continue
387
+
388
+ logger.info(f"Successfully loaded {loaded_count} clients from {client_json_path}")
389
+
390
+ except json.JSONDecodeError as e:
391
+ logger.error(f"Invalid JSON in client storage file {client_json_path}: {e}")
324
392
  except Exception as e:
325
393
  logger.error(f"Failed to load clients from {client_json_path}: {e}")
@@ -19,6 +19,7 @@ from d365fo_client.mcp.fastmcp_utils import create_default_profile_if_needed, lo
19
19
  from d365fo_client.profile_manager import ProfileManager
20
20
  from d365fo_client.settings import get_settings
21
21
  from mcp.server.auth.settings import AuthSettings,ClientRegistrationOptions
22
+ from d365fo_client.mcp.auth_server.auth.providers.apikey import APIKeyVerifier
22
23
 
23
24
 
24
25
 
@@ -120,6 +121,7 @@ Environment Variables:
120
121
  D365FO_MCP_AUTH_TENANT_ID Azure AD tenant ID for authentication
121
122
  D365FO_MCP_AUTH_BASE_URL http://localhost:8000
122
123
  D365FO_MCP_AUTH_REQUIRED_SCOPES User.Read,email,openid,profile
124
+ D365FO_MCP_API_KEY_VALUE API key for authentication (send as: Authorization: Bearer <key>)
123
125
  D365FO_LOG_LEVEL Logging level (DEBUG, INFO, WARNING, ERROR)
124
126
  D365FO_LOG_FILE Custom log file path (default: ~/.d365fo-mcp/logs/fastmcp-server.log)
125
127
  D365FO_META_CACHE_DIR Metadata cache directory (default: ~/.d365fo-mcp/cache)
@@ -254,63 +256,93 @@ logger.info(f"Startup Mode: {settings.get_startup_mode()}")
254
256
  logger.info(f"Client Credentials: {'Configured' if settings.has_client_credentials() else 'Not configured'}")
255
257
  logger.info("====================================")
256
258
 
257
- if is_remote_transport and not settings.has_mcp_auth_credentials():
259
+ # Validate authentication for remote transports
260
+ if is_remote_transport:
261
+ has_oauth = settings.has_mcp_auth_credentials()
262
+ has_api_key = settings.has_mcp_api_key_auth()
263
+
264
+ # Must have either OAuth or API key
265
+ if not has_oauth and not has_api_key:
266
+ logger.error(
267
+ "Error: Remote transports (SSE/HTTP) require authentication. "
268
+ "Please configure either:\n"
269
+ " OAuth: D365FO_MCP_AUTH_CLIENT_ID, D365FO_MCP_AUTH_CLIENT_SECRET, "
270
+ "D365FO_MCP_AUTH_TENANT_ID, D365FO_MCP_AUTH_BASE_URL, D365FO_MCP_AUTH_REQUIRED_SCOPES\n"
271
+ " OR\n"
272
+ " API Key: D365FO_MCP_API_KEY_VALUE, D365FO_MCP_API_KEY_HEADER_NAME (optional)"
273
+ )
274
+ sys.exit(1)
258
275
 
259
- logger.error("Warning: Client credentials (D365FO_MCP_AUTH_CLIENT_ID and D365FO_MCP_AUTH_CLIENT_SECRET, D365FO_MCP_AUTH_TENANT_ID,D365FO_MCP_AUTH_BASE_URL and D365FO_MCP_AUTH_REQUIRED_SCOPES) are not set. " +
260
- "Remote transports require authentication to the D365FO environment.")
261
- sys.exit(1)
276
+ # OAuth takes precedence if both are configured
277
+ if has_oauth and has_api_key:
278
+ logger.warning(
279
+ "Both OAuth and API Key authentication configured. "
280
+ "Using OAuth (takes precedence)."
281
+ )
262
282
 
263
- auth_provider: AzureProvider | None = None
283
+ # Initialize authentication provider
284
+ auth_provider: AzureProvider | APIKeyVerifier | None = None # type: ignore
264
285
  auth: AuthSettings | None = None
265
286
 
266
287
  if is_remote_transport:
267
- assert settings.mcp_auth_client_id is not None
268
- assert settings.mcp_auth_client_secret is not None
269
- assert settings.mcp_auth_tenant_id is not None
270
- assert settings.mcp_auth_base_url is not None
271
- assert settings.mcp_auth_required_scopes is not None
272
- required_scopes=settings.mcp_auth_required_scopes_list()
273
-
274
- # if "AX.FullAccess" not in required_scopes:
275
- # logger.warning("Warning: 'AX.FullAccess' scope is not supported. Adding it automatically.")
276
- # required_scopes.append("https://erp.dynamics.com/AX.FullAccess")
277
-
278
- # Initialize authorization settings
279
- auth_provider = AzureProvider(
280
- client_id=settings.mcp_auth_client_id, # Your Azure App Client ID
281
- client_secret=settings.mcp_auth_client_secret, # Your Azure App Client Secret
282
- tenant_id=settings.mcp_auth_tenant_id, # Your Azure Tenant ID (REQUIRED)
283
- base_url=settings.mcp_auth_base_url, # Must match your App registration
284
- required_scopes=required_scopes or ["User.Read"], # type: ignore # Scopes your app needs
285
- redirect_path="/auth/callback", # Ensure callback path is explicit
286
- clients_storage_path=config_path or ...
287
- )
288
- from mcp.shared.auth import OAuthClientInformationFull
289
-
290
- # auth_provider._clients["eae68fdd-610b-47c5-9907-709c7452f1b3"] = OAuthClientInformationFull(
291
- # client_id="5eae68fdd-610b-47c5-9907-709c7452f1b3",
292
- # client_name="Test Client",
293
- # redirect_uris=[AnyUrl("http://127.0.0.1:33418")],
294
- # scope=",".join(required_scopes)
295
- # )
296
-
288
+ has_oauth = settings.has_mcp_auth_credentials()
289
+ has_api_key = settings.has_mcp_api_key_auth()
290
+
291
+ if has_oauth:
292
+ # OAuth authentication setup
293
+ logger.info("Initializing OAuth authentication with Azure AD")
294
+
295
+ assert settings.mcp_auth_client_id is not None
296
+ assert settings.mcp_auth_client_secret is not None
297
+ assert settings.mcp_auth_tenant_id is not None
298
+ assert settings.mcp_auth_base_url is not None
299
+ assert settings.mcp_auth_required_scopes is not None
300
+ required_scopes = settings.mcp_auth_required_scopes_list()
301
+
302
+ # Initialize authorization settings
303
+ auth_provider = AzureProvider(
304
+ client_id=settings.mcp_auth_client_id,
305
+ client_secret=settings.mcp_auth_client_secret,
306
+ tenant_id=settings.mcp_auth_tenant_id,
307
+ base_url=settings.mcp_auth_base_url,
308
+ required_scopes=required_scopes or ["User.Read"], # type: ignore
309
+ redirect_path="/auth/callback",
310
+ clients_storage_path=config_path or ...
311
+ )
297
312
 
298
- auth=AuthSettings(
313
+ auth = AuthSettings(
299
314
  issuer_url=AnyHttpUrl(settings.mcp_auth_base_url),
300
315
  client_registration_options=ClientRegistrationOptions(
301
316
  enabled=True,
302
- valid_scopes=required_scopes or ["User.Read"], # type: ignore
303
- default_scopes=required_scopes or ["User.Read"], # type: ignore
317
+ valid_scopes=required_scopes or ["User.Read"], # type: ignore
318
+ default_scopes=required_scopes or ["User.Read"], # type: ignore
304
319
  ),
305
- required_scopes=required_scopes or ["User.Read"], # type: ignore
320
+ required_scopes=required_scopes or ["User.Read"], # type: ignore
306
321
  resource_server_url=AnyHttpUrl(settings.mcp_auth_base_url),
307
-
322
+ )
323
+
324
+ elif has_api_key:
325
+ # API Key authentication setup
326
+ logger.info("Initializing API Key authentication")
327
+
328
+ from d365fo_client.mcp.auth_server.auth.providers.apikey import APIKeyVerifier
329
+
330
+ auth_provider = APIKeyVerifier( # type: ignore
331
+ api_key=settings.mcp_api_key_value, # type: ignore
332
+ base_url=settings.mcp_auth_base_url,
333
+ )
334
+
335
+ # For API Key authentication
336
+ auth = AuthSettings(
337
+ issuer_url=AnyHttpUrl(settings.mcp_auth_base_url) if settings.mcp_auth_base_url else AnyHttpUrl("http://localhost"),
338
+ resource_server_url=None
308
339
  )
309
340
 
310
341
  # Initialize FastMCP server with configuration
311
342
  mcp = FastMCP(
312
343
  name=server_config.get("name", "d365fo-mcp-server"),
313
- auth_server_provider=auth_provider,
344
+ auth_server_provider=auth_provider if isinstance(auth_provider, AzureProvider) else None,
345
+ token_verifier=auth_provider if isinstance(auth_provider, APIKeyVerifier) else None,
314
346
  auth=auth,
315
347
  instructions=server_config.get(
316
348
  "instructions",
@@ -326,16 +358,33 @@ mcp = FastMCP(
326
358
 
327
359
  )
328
360
 
329
- if is_remote_transport:
361
+ # Add OAuth callback route only for Azure OAuth provider
362
+ if is_remote_transport and isinstance(auth_provider, AzureProvider):
330
363
  from starlette.requests import Request
331
364
  from starlette.responses import RedirectResponse
332
-
365
+
333
366
  @mcp.custom_route(path=auth_provider._redirect_path, methods=["GET"]) # type: ignore
334
367
  async def handle_idp_callback(request: Request) -> RedirectResponse:
335
368
  return await auth_provider._handle_idp_callback(request) # type: ignore
336
369
 
370
+ # Initialize FastD365FOMCPServer
337
371
  server = FastD365FOMCPServer(mcp, config, profile_manager=profile_manager)
338
372
 
373
+ # Configure API Key authentication if enabled
374
+ if is_remote_transport:
375
+ from d365fo_client.mcp.auth_server.auth.providers.apikey import APIKeyVerifier
376
+
377
+ if isinstance(auth_provider, APIKeyVerifier):
378
+
379
+ logger.info("API Key authentication configured successfully")
380
+ logger.info("=" * 60)
381
+ logger.info("IMPORTANT: Clients must authenticate using:")
382
+ logger.info(" Authorization: Bearer <your-api-key>")
383
+ logger.info("")
384
+ logger.info("Example:")
385
+ logger.info(f" curl -H 'Authorization: Bearer YOUR_KEY' http://localhost:{transport_config.get('http', {}).get('port', 8000)}/")
386
+ logger.info("=" * 60)
387
+
339
388
  logger.info("FastD365FOMCPServer initialized successfully")
340
389
 
341
390
 
d365fo_client/settings.py CHANGED
@@ -5,7 +5,7 @@ from enum import Enum
5
5
  from pathlib import Path
6
6
  from typing import Literal, Optional
7
7
 
8
- from pydantic import Field, field_validator
8
+ from pydantic import Field, SecretStr, field_validator
9
9
  from pydantic_settings import BaseSettings, SettingsConfigDict
10
10
 
11
11
  from .utils import get_default_cache_directory
@@ -104,7 +104,15 @@ class D365FOSettings(BaseSettings):
104
104
  description="MCP authentication required scopes (comma-separated)",
105
105
  alias="D365FO_MCP_AUTH_REQUIRED_SCOPES"
106
106
  )
107
-
107
+
108
+ # === MCP API Key Authentication Settings ===
109
+
110
+ mcp_api_key_value: Optional[SecretStr] = Field(
111
+ default=None,
112
+ description="API key value for authentication (send as Authorization: Bearer <key>)",
113
+ alias="D365FO_MCP_API_KEY_VALUE"
114
+ )
115
+
108
116
  # === MCP Server Transport Settings ===
109
117
 
110
118
  mcp_transport: TransportProtocol = Field(
@@ -285,6 +293,10 @@ class D365FOSettings(BaseSettings):
285
293
  def has_mcp_auth_credentials(self) -> bool:
286
294
  """Check if MCP authentication credentials are configured."""
287
295
  return all([self.mcp_auth_client_id, self.mcp_auth_client_secret, self.mcp_auth_tenant_id])
296
+
297
+ def has_mcp_api_key_auth(self) -> bool:
298
+ """Check if API key authentication is configured."""
299
+ return self.mcp_api_key_value is not None
288
300
 
289
301
  def get_startup_mode(self) -> Literal["profile_only", "default_auth", "client_credentials"]:
290
302
  """Determine startup mode based on configuration."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d365fo-client
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Microsoft Dynamics 365 Finance & Operations client
5
5
  Author-email: Muhammad Afzaal <mo@thedataguy.pro>
6
6
  License-Expression: MIT
@@ -15,12 +15,12 @@ d365fo_client/profile_manager.py,sha256=43_V82r0gugEgn5o2EbGN-JDleJaEFMPH9Ak4KmJ
15
15
  d365fo_client/profiles.py,sha256=3EwD9zYfyjdiuPPaul-_qvL8GUPiky_m6kyZqoYN20I,8106
16
16
  d365fo_client/query.py,sha256=wOZjXEtGzPcd5mRfdkpMZTHZdSId44gLSboJs4LeSaw,5028
17
17
  d365fo_client/session.py,sha256=4OIR61eYQHALPmbXZ446Ko4j5ttvozDxDRnMikogBys,1841
18
- d365fo_client/settings.py,sha256=Pq5GnJA6B15RDKCbaIR8XQQ2Leys3RvqI1sfm3f2qcA,10854
18
+ d365fo_client/settings.py,sha256=kkoB1UUo2Hwkz42hlpj35g_ih-BaP6vHCLPRea6N_10,11284
19
19
  d365fo_client/sync_models.py,sha256=BOYR3zCaqPwrvAQq2pM3cBb6Pt9SU7pxKKTAJLsoVZA,9247
20
20
  d365fo_client/utils.py,sha256=JywzDfgEI4Ei3l6xnAA0aw15RThWAT8mY-C4tpTrToU,6846
21
21
  d365fo_client/mcp/__init__.py,sha256=IT55UrOYT3kC7DS5zlALPilVu_KEIlVgIm3WR7QmEcQ,673
22
22
  d365fo_client/mcp/client_manager.py,sha256=LoCpRl_WeURXVjv8ugBFFlWqQ3bUO-HpP94CwC5c010,9705
23
- d365fo_client/mcp/fastmcp_main.py,sha256=0kzxugFnqRyiiWaEmeX2_ZSmnn5TzJKwB_jeRiXafC8,13934
23
+ d365fo_client/mcp/fastmcp_main.py,sha256=ofyYvez5UE9hVQ4q73us--oSrKbqTpj0t0MfaVJaWcA,15787
24
24
  d365fo_client/mcp/fastmcp_server.py,sha256=xm1gugM8TX0n613n9MqRFDlBL9pK2-5jsM2kpbsCt9o,23873
25
25
  d365fo_client/mcp/fastmcp_utils.py,sha256=uBg4iziIEVN-MiMxtMwSg8a4RPseW6ltC-HKdGq-dWw,18250
26
26
  d365fo_client/mcp/main.py,sha256=sNG6IIs3SvzumUhfzRH6aNzN2T7OZrPNl7_Yx-Fkt8I,5333
@@ -33,7 +33,8 @@ d365fo_client/mcp/auth_server/auth/auth.py,sha256=YLM04cwXHURaI1o3liPhAw-37YAnDO
33
33
  d365fo_client/mcp/auth_server/auth/oauth_proxy.py,sha256=EOveccoyF6wkRuf1ccQ-3QHH1n8JGzinKzbrbyodQHs,42253
34
34
  d365fo_client/mcp/auth_server/auth/redirect_validation.py,sha256=Jlhela9xpTbw4aWnQ04A5Z-TW0HYOC3f9BMsq3NXx1Q,2000
35
35
  d365fo_client/mcp/auth_server/auth/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- d365fo_client/mcp/auth_server/auth/providers/azure.py,sha256=MyOkyz_c19MB9RrcaM-mXuTbIlRK-Z5Z-o2AnT7N-jk,12824
36
+ d365fo_client/mcp/auth_server/auth/providers/apikey.py,sha256=g9ZvzE40fjRFrth-0FhFiBLFSfCFV2uV5jIBlDr0F2Y,2960
37
+ d365fo_client/mcp/auth_server/auth/providers/azure.py,sha256=3oyVz6AAoHNQP7UKnSmNA9jDOQY9kNbgawgE181vF_4,16032
37
38
  d365fo_client/mcp/auth_server/auth/providers/bearer.py,sha256=uz7sbxXQobem3YV6HUSf-QUOnkhhnmjyXoUEu5AIIkc,891
38
39
  d365fo_client/mcp/auth_server/auth/providers/jwt.py,sha256=Xj9W8WsENjeT8U3Ncic69gxQREkPp1P6LxQodE3UI5A,19542
39
40
  d365fo_client/mcp/mixins/__init__.py,sha256=b7sJ77m-7EmT5PKEuMkIIBAI6ISTG2-ZnNNQQBVYbTc,778
@@ -76,9 +77,9 @@ d365fo_client/metadata_v2/search_engine_v2.py,sha256=s_XVqP3LLog19IAv8DpxVUS7TFR
76
77
  d365fo_client/metadata_v2/sync_manager_v2.py,sha256=JM8ThiXyXKYlnGaH3iwbBlPd9RFkDr0-ivkuHLAIo40,34090
77
78
  d365fo_client/metadata_v2/sync_session_manager.py,sha256=XWxjFGwgnm6S4lzeYqyPqZgvpcqzK3tBaxnQ-srgYrg,44365
78
79
  d365fo_client/metadata_v2/version_detector.py,sha256=t9mKaeT4SKb13LmIq5fB6PTLOZn5Jp7ZUqQobntNEUg,15791
79
- d365fo_client-0.3.0.dist-info/licenses/LICENSE,sha256=idD7NJAZD7ognzZVyKjDxVYDCmngEIt0WxA_uB1v0iI,1071
80
- d365fo_client-0.3.0.dist-info/METADATA,sha256=AbDB8_EoFT_MaATG9HDXXPvIQ16sJ3dAw78HrLICB2s,59480
81
- d365fo_client-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- d365fo_client-0.3.0.dist-info/entry_points.txt,sha256=jCw6TT6lLRKzAWTSzxMhdYbTT-m5v0LDcZ9Zun4JM-0,174
83
- d365fo_client-0.3.0.dist-info/top_level.txt,sha256=ZbvqO90RjhOW0cjFCAEeP8OFyITbhrij2vC3k4bWERQ,14
84
- d365fo_client-0.3.0.dist-info/RECORD,,
80
+ d365fo_client-0.3.1.dist-info/licenses/LICENSE,sha256=idD7NJAZD7ognzZVyKjDxVYDCmngEIt0WxA_uB1v0iI,1071
81
+ d365fo_client-0.3.1.dist-info/METADATA,sha256=ogDZb7EH-gowVAWEGVBpx2MPk7le7ZcgfyZT6ZtHnow,59480
82
+ d365fo_client-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
+ d365fo_client-0.3.1.dist-info/entry_points.txt,sha256=jCw6TT6lLRKzAWTSzxMhdYbTT-m5v0LDcZ9Zun4JM-0,174
84
+ d365fo_client-0.3.1.dist-info/top_level.txt,sha256=ZbvqO90RjhOW0cjFCAEeP8OFyITbhrij2vC3k4bWERQ,14
85
+ d365fo_client-0.3.1.dist-info/RECORD,,