d365fo-client 0.2.4__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.
- d365fo_client/__init__.py +7 -1
- d365fo_client/auth.py +9 -21
- d365fo_client/cli.py +25 -13
- d365fo_client/client.py +8 -4
- d365fo_client/config.py +52 -30
- d365fo_client/credential_sources.py +5 -0
- d365fo_client/main.py +1 -1
- d365fo_client/mcp/__init__.py +3 -1
- d365fo_client/mcp/auth_server/__init__.py +5 -0
- d365fo_client/mcp/auth_server/auth/__init__.py +30 -0
- d365fo_client/mcp/auth_server/auth/auth.py +372 -0
- d365fo_client/mcp/auth_server/auth/oauth_proxy.py +989 -0
- d365fo_client/mcp/auth_server/auth/providers/__init__.py +0 -0
- d365fo_client/mcp/auth_server/auth/providers/apikey.py +83 -0
- d365fo_client/mcp/auth_server/auth/providers/azure.py +393 -0
- d365fo_client/mcp/auth_server/auth/providers/bearer.py +25 -0
- d365fo_client/mcp/auth_server/auth/providers/jwt.py +547 -0
- d365fo_client/mcp/auth_server/auth/redirect_validation.py +65 -0
- d365fo_client/mcp/auth_server/dependencies.py +136 -0
- d365fo_client/mcp/client_manager.py +16 -67
- d365fo_client/mcp/fastmcp_main.py +407 -0
- d365fo_client/mcp/fastmcp_server.py +598 -0
- d365fo_client/mcp/fastmcp_utils.py +431 -0
- d365fo_client/mcp/main.py +40 -13
- d365fo_client/mcp/mixins/__init__.py +24 -0
- d365fo_client/mcp/mixins/base_tools_mixin.py +55 -0
- d365fo_client/mcp/mixins/connection_tools_mixin.py +50 -0
- d365fo_client/mcp/mixins/crud_tools_mixin.py +311 -0
- d365fo_client/mcp/mixins/database_tools_mixin.py +685 -0
- d365fo_client/mcp/mixins/label_tools_mixin.py +87 -0
- d365fo_client/mcp/mixins/metadata_tools_mixin.py +565 -0
- d365fo_client/mcp/mixins/performance_tools_mixin.py +109 -0
- d365fo_client/mcp/mixins/profile_tools_mixin.py +713 -0
- d365fo_client/mcp/mixins/sync_tools_mixin.py +321 -0
- d365fo_client/mcp/prompts/action_execution.py +1 -1
- d365fo_client/mcp/prompts/sequence_analysis.py +1 -1
- d365fo_client/mcp/tools/crud_tools.py +3 -3
- d365fo_client/mcp/tools/sync_tools.py +1 -1
- d365fo_client/mcp/utilities/__init__.py +1 -0
- d365fo_client/mcp/utilities/auth.py +34 -0
- d365fo_client/mcp/utilities/logging.py +58 -0
- d365fo_client/mcp/utilities/types.py +426 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +2 -0
- d365fo_client/metadata_v2/sync_session_manager.py +7 -7
- d365fo_client/models.py +139 -139
- d365fo_client/output.py +2 -2
- d365fo_client/profile_manager.py +62 -27
- d365fo_client/profiles.py +118 -113
- d365fo_client/settings.py +367 -0
- d365fo_client/sync_models.py +85 -2
- d365fo_client/utils.py +2 -1
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.1.dist-info}/METADATA +273 -18
- d365fo_client-0.3.1.dist-info/RECORD +85 -0
- d365fo_client-0.3.1.dist-info/entry_points.txt +4 -0
- d365fo_client-0.2.4.dist-info/RECORD +0 -56
- d365fo_client-0.2.4.dist-info/entry_points.txt +0 -3
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.1.dist-info}/WHEEL +0 -0
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.1.dist-info}/top_level.txt +0 -0
File without changes
|
@@ -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
|
@@ -0,0 +1,393 @@
|
|
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
|
+
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
|
293
|
+
|
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
|
+
"""
|
301
|
+
if not self.clients_storage_path:
|
302
|
+
logger.warning("No clients storage path configured. Skipping client save.")
|
303
|
+
return
|
304
|
+
|
305
|
+
try:
|
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
|
+
|
312
|
+
# Convert OAuthClientInformationFull objects to dictionaries for JSON serialization
|
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
|
+
|
337
|
+
except Exception as e:
|
338
|
+
logger.error(f"Failed to save client data to {self.clients_storage_path}: {e}")
|
339
|
+
raise
|
340
|
+
|
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
|
+
"""
|
347
|
+
if not self.clients_storage_path:
|
348
|
+
logger.debug("No clients storage path configured. Skipping client load.")
|
349
|
+
return
|
350
|
+
|
351
|
+
try:
|
352
|
+
client_json_path = Path(self.clients_storage_path) / "clients.json"
|
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}")
|
392
|
+
except Exception as e:
|
393
|
+
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
|
+
# )
|