fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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 (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,34 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
3
+ import time
4
4
  import webbrowser
5
- from asyncio import Future
6
5
  from collections.abc import AsyncGenerator
7
- from datetime import datetime, timedelta, timezone
8
- from pathlib import Path
9
- from typing import Any, Literal
6
+ from typing import Any
10
7
  from urllib.parse import urlparse
11
8
 
12
9
  import anyio
13
10
  import httpx
11
+ from key_value.aio.adapters.pydantic import PydanticAdapter
12
+ from key_value.aio.protocols import AsyncKeyValue
13
+ from key_value.aio.stores.memory import MemoryStore
14
14
  from mcp.client.auth import OAuthClientProvider, TokenStorage
15
+ from mcp.shared._httpx_utils import McpHttpClientFactory
15
16
  from mcp.shared.auth import (
16
17
  OAuthClientInformationFull,
17
18
  OAuthClientMetadata,
19
+ OAuthToken,
18
20
  )
19
- from mcp.shared.auth import (
20
- OAuthToken as OAuthToken,
21
- )
22
- from pydantic import AnyHttpUrl, BaseModel, TypeAdapter, ValidationError
21
+ from pydantic import AnyHttpUrl
22
+ from typing_extensions import override
23
23
  from uvicorn.server import Server
24
24
 
25
- from fastmcp import settings as fastmcp_global_settings
26
25
  from fastmcp.client.oauth_callback import (
26
+ OAuthCallbackResult,
27
27
  create_oauth_callback_server,
28
28
  )
29
29
  from fastmcp.utilities.http import find_available_port
30
30
  from fastmcp.utilities.logging import get_logger
31
- from fastmcp.utilities.storage import JSONFileStorage
32
31
 
33
32
  __all__ = ["OAuth"]
34
33
 
@@ -38,176 +37,6 @@ logger = get_logger(__name__)
38
37
  class ClientNotFoundError(Exception):
39
38
  """Raised when OAuth client credentials are not found on the server."""
40
39
 
41
- pass
42
-
43
-
44
- class StoredToken(BaseModel):
45
- """Token storage format with absolute expiry time."""
46
-
47
- token_payload: OAuthToken
48
- expires_at: datetime | None
49
-
50
-
51
- # Create TypeAdapter at module level for efficient parsing
52
- stored_token_adapter = TypeAdapter(StoredToken)
53
-
54
-
55
- def default_cache_dir() -> Path:
56
- return fastmcp_global_settings.home / "oauth-mcp-client-cache"
57
-
58
-
59
- class FileTokenStorage(TokenStorage):
60
- """
61
- File-based token storage implementation for OAuth credentials and tokens.
62
- Implements the mcp.client.auth.TokenStorage protocol.
63
-
64
- Each instance is tied to a specific server URL for proper token isolation.
65
- Uses JSONFileStorage internally for consistent file handling.
66
- """
67
-
68
- def __init__(self, server_url: str, cache_dir: Path | None = None):
69
- """Initialize storage for a specific server URL."""
70
- self.server_url = server_url
71
- # Use JSONFileStorage for actual file operations
72
- self._storage = JSONFileStorage(cache_dir or default_cache_dir())
73
-
74
- @staticmethod
75
- def get_base_url(url: str) -> str:
76
- """Extract the base URL (scheme + host) from a URL."""
77
- parsed = urlparse(url)
78
- return f"{parsed.scheme}://{parsed.netloc}"
79
-
80
- def _get_storage_key(self, file_type: Literal["client_info", "tokens"]) -> str:
81
- """Get the storage key for the specified data type.
82
-
83
- JSONFileStorage will handle making the key filesystem-safe.
84
- """
85
- base_url = self.get_base_url(self.server_url)
86
- return f"{base_url}_{file_type}"
87
-
88
- def _get_file_path(self, file_type: Literal["client_info", "tokens"]) -> Path:
89
- """Get the file path for the specified cache file type.
90
-
91
- This method is kept for backward compatibility with tests that access _get_file_path.
92
- """
93
- key = self._get_storage_key(file_type)
94
- return self._storage._get_file_path(key)
95
-
96
- async def get_tokens(self) -> OAuthToken | None:
97
- """Load tokens from file storage."""
98
- key = self._get_storage_key("tokens")
99
- data = await self._storage.get(key)
100
-
101
- if data is None:
102
- return None
103
-
104
- try:
105
- # Parse and validate as StoredToken
106
- stored = stored_token_adapter.validate_python(data)
107
-
108
- # Check if token is expired
109
- if stored.expires_at is not None:
110
- now = datetime.now(timezone.utc)
111
- if now >= stored.expires_at:
112
- logger.debug(
113
- f"Token expired for {self.get_base_url(self.server_url)}"
114
- )
115
- return None
116
-
117
- # Recalculate expires_in to be correct relative to now
118
- if stored.token_payload.expires_in is not None:
119
- remaining = stored.expires_at - now
120
- stored.token_payload.expires_in = max(
121
- 0, int(remaining.total_seconds())
122
- )
123
-
124
- return stored.token_payload
125
-
126
- except ValidationError as e:
127
- logger.debug(
128
- f"Could not validate tokens for {self.get_base_url(self.server_url)}: {e}"
129
- )
130
- return None
131
-
132
- async def set_tokens(self, tokens: OAuthToken) -> None:
133
- """Save tokens to file storage."""
134
- key = self._get_storage_key("tokens")
135
-
136
- # Calculate absolute expiry time if expires_in is present
137
- expires_at = None
138
- if tokens.expires_in is not None:
139
- expires_at = datetime.now(timezone.utc) + timedelta(
140
- seconds=tokens.expires_in
141
- )
142
-
143
- # Create StoredToken and save using storage
144
- # Note: JSONFileStorage will wrap this in {"data": ..., "timestamp": ...}
145
- stored = StoredToken(token_payload=tokens, expires_at=expires_at)
146
- await self._storage.set(key, stored.model_dump(mode="json"))
147
- logger.debug(f"Saved tokens for {self.get_base_url(self.server_url)}")
148
-
149
- async def get_client_info(self) -> OAuthClientInformationFull | None:
150
- """Load client information from file storage."""
151
- key = self._get_storage_key("client_info")
152
- data = await self._storage.get(key)
153
-
154
- if data is None:
155
- return None
156
-
157
- try:
158
- client_info = OAuthClientInformationFull.model_validate(data)
159
- # Check if we have corresponding valid tokens
160
- # If no tokens exist, the OAuth flow was incomplete and we should
161
- # force a fresh client registration
162
- tokens = await self.get_tokens()
163
- if tokens is None:
164
- logger.debug(
165
- f"No tokens found for client info at {self.get_base_url(self.server_url)}. "
166
- "OAuth flow may have been incomplete. Clearing client info to force fresh registration."
167
- )
168
- # Clear the incomplete client info
169
- await self._storage.delete(key)
170
- return None
171
-
172
- return client_info
173
- except ValidationError as e:
174
- logger.debug(
175
- f"Could not validate client info for {self.get_base_url(self.server_url)}: {e}"
176
- )
177
- return None
178
-
179
- async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
180
- """Save client information to file storage."""
181
- key = self._get_storage_key("client_info")
182
- await self._storage.set(key, client_info.model_dump(mode="json"))
183
- logger.debug(f"Saved client info for {self.get_base_url(self.server_url)}")
184
-
185
- def clear(self) -> None:
186
- """Clear all cached data for this server.
187
-
188
- Note: This is a synchronous method for backward compatibility.
189
- Uses direct file operations instead of async storage methods.
190
- """
191
- file_types: list[Literal["client_info", "tokens"]] = ["client_info", "tokens"]
192
- for file_type in file_types:
193
- # Use the file path directly for synchronous deletion
194
- path = self._get_file_path(file_type)
195
- path.unlink(missing_ok=True)
196
- logger.debug(f"Cleared OAuth cache for {self.get_base_url(self.server_url)}")
197
-
198
- @classmethod
199
- def clear_all(cls, cache_dir: Path | None = None) -> None:
200
- """Clear all cached data for all servers."""
201
- cache_dir = cache_dir or default_cache_dir()
202
- if not cache_dir.exists():
203
- return
204
-
205
- file_types: list[Literal["client_info", "tokens"]] = ["client_info", "tokens"]
206
- for file_type in file_types:
207
- for file in cache_dir.glob(f"*_{file_type}.json"):
208
- file.unlink(missing_ok=True)
209
- logger.info("Cleared all OAuth client cache data.")
210
-
211
40
 
212
41
  async def check_if_auth_required(
213
42
  mcp_url: str, httpx_kwargs: dict[str, Any] | None = None
@@ -228,7 +57,7 @@ async def check_if_auth_required(
228
57
  return True
229
58
 
230
59
  # Check for WWW-Authenticate header
231
- if "WWW-Authenticate" in response.headers:
60
+ if "WWW-Authenticate" in response.headers: # noqa: SIM103
232
61
  return True
233
62
 
234
63
  # If we get a successful response, auth may not be required
@@ -239,6 +68,70 @@ async def check_if_auth_required(
239
68
  return True
240
69
 
241
70
 
71
+ class TokenStorageAdapter(TokenStorage):
72
+ _server_url: str
73
+ _key_value_store: AsyncKeyValue
74
+ _storage_oauth_token: PydanticAdapter[OAuthToken]
75
+ _storage_client_info: PydanticAdapter[OAuthClientInformationFull]
76
+
77
+ def __init__(self, async_key_value: AsyncKeyValue, server_url: str):
78
+ self._server_url = server_url
79
+ self._key_value_store = async_key_value
80
+ self._storage_oauth_token = PydanticAdapter[OAuthToken](
81
+ default_collection="mcp-oauth-token",
82
+ key_value=async_key_value,
83
+ pydantic_model=OAuthToken,
84
+ raise_on_validation_error=True,
85
+ )
86
+ self._storage_client_info = PydanticAdapter[OAuthClientInformationFull](
87
+ default_collection="mcp-oauth-client-info",
88
+ key_value=async_key_value,
89
+ pydantic_model=OAuthClientInformationFull,
90
+ raise_on_validation_error=True,
91
+ )
92
+
93
+ def _get_token_cache_key(self) -> str:
94
+ return f"{self._server_url}/tokens"
95
+
96
+ def _get_client_info_cache_key(self) -> str:
97
+ return f"{self._server_url}/client_info"
98
+
99
+ async def clear(self) -> None:
100
+ await self._storage_oauth_token.delete(key=self._get_token_cache_key())
101
+ await self._storage_client_info.delete(key=self._get_client_info_cache_key())
102
+
103
+ @override
104
+ async def get_tokens(self) -> OAuthToken | None:
105
+ return await self._storage_oauth_token.get(key=self._get_token_cache_key())
106
+
107
+ @override
108
+ async def set_tokens(self, tokens: OAuthToken) -> None:
109
+ await self._storage_oauth_token.put(
110
+ key=self._get_token_cache_key(),
111
+ value=tokens,
112
+ ttl=tokens.expires_in,
113
+ )
114
+
115
+ @override
116
+ async def get_client_info(self) -> OAuthClientInformationFull | None:
117
+ return await self._storage_client_info.get(
118
+ key=self._get_client_info_cache_key()
119
+ )
120
+
121
+ @override
122
+ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
123
+ ttl: int | None = None
124
+
125
+ if client_info.client_secret_expires_at:
126
+ ttl = client_info.client_secret_expires_at - int(time.time())
127
+
128
+ await self._storage_client_info.put(
129
+ key=self._get_client_info_cache_key(),
130
+ value=client_info,
131
+ ttl=ttl,
132
+ )
133
+
134
+
242
135
  class OAuth(OAuthClientProvider):
243
136
  """
244
137
  OAuth client provider for MCP servers with browser-based authentication.
@@ -252,9 +145,10 @@ class OAuth(OAuthClientProvider):
252
145
  mcp_url: str,
253
146
  scopes: str | list[str] | None = None,
254
147
  client_name: str = "FastMCP Client",
255
- token_storage_cache_dir: Path | None = None,
148
+ token_storage: AsyncKeyValue | None = None,
256
149
  additional_client_metadata: dict[str, Any] | None = None,
257
150
  callback_port: int | None = None,
151
+ httpx_client_factory: McpHttpClientFactory | None = None,
258
152
  ):
259
153
  """
260
154
  Initialize OAuth client provider for an MCP server.
@@ -264,7 +158,7 @@ class OAuth(OAuthClientProvider):
264
158
  scopes: OAuth scopes to request. Can be a
265
159
  space-separated string or a list of strings.
266
160
  client_name: Name for this client during registration
267
- token_storage_cache_dir: Directory for FileTokenStorage
161
+ token_storage: An AsyncKeyValue-compatible token store, tokens are stored in memory if not provided
268
162
  additional_client_metadata: Extra fields for OAuthClientMetadata
269
163
  callback_port: Fixed port for OAuth callback (default: random available port)
270
164
  """
@@ -272,6 +166,7 @@ class OAuth(OAuthClientProvider):
272
166
  server_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
273
167
 
274
168
  # Setup OAuth client
169
+ self.httpx_client_factory = httpx_client_factory or httpx.AsyncClient
275
170
  self.redirect_port = callback_port or find_available_port()
276
171
  redirect_uri = f"http://localhost:{self.redirect_port}/callback"
277
172
 
@@ -294,8 +189,20 @@ class OAuth(OAuthClientProvider):
294
189
  )
295
190
 
296
191
  # Create server-specific token storage
297
- storage = FileTokenStorage(
298
- server_url=server_base_url, cache_dir=token_storage_cache_dir
192
+ token_storage = token_storage or MemoryStore()
193
+
194
+ if isinstance(token_storage, MemoryStore):
195
+ from warnings import warn
196
+
197
+ warn(
198
+ message="Using in-memory token storage -- tokens will be lost when the client restarts. "
199
+ + "For persistent storage across multiple MCP servers, provide an encrypted AsyncKeyValue backend. "
200
+ + "See https://gofastmcp.com/clients/auth/oauth#token-storage for details.",
201
+ stacklevel=2,
202
+ )
203
+
204
+ self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(
205
+ async_key_value=token_storage, server_url=server_base_url
299
206
  )
300
207
 
301
208
  # Store server_base_url for use in callback_handler
@@ -305,7 +212,7 @@ class OAuth(OAuthClientProvider):
305
212
  super().__init__(
306
213
  server_url=server_base_url,
307
214
  client_metadata=client_metadata,
308
- storage=storage,
215
+ storage=self.token_storage_adapter,
309
216
  redirect_handler=self.redirect_handler,
310
217
  callback_handler=self.callback_handler,
311
218
  )
@@ -322,7 +229,7 @@ class OAuth(OAuthClientProvider):
322
229
  async def redirect_handler(self, authorization_url: str) -> None:
323
230
  """Open browser for authorization, with pre-flight check for invalid client."""
324
231
  # Pre-flight check to detect invalid client_id before opening browser
325
- async with httpx.AsyncClient() as client:
232
+ async with self.httpx_client_factory() as client:
326
233
  response = await client.get(authorization_url, follow_redirects=False)
327
234
 
328
235
  # Check for client not found error (400 typically means bad client_id)
@@ -342,14 +249,16 @@ class OAuth(OAuthClientProvider):
342
249
 
343
250
  async def callback_handler(self) -> tuple[str, str | None]:
344
251
  """Handle OAuth callback and return (auth_code, state)."""
345
- # Create a future to capture the OAuth response
346
- response_future: Future[Any] = asyncio.get_running_loop().create_future()
252
+ # Create result container and event to capture the OAuth response
253
+ result = OAuthCallbackResult()
254
+ result_ready = anyio.Event()
347
255
 
348
- # Create server with the future
256
+ # Create server with result tracking
349
257
  server: Server = create_oauth_callback_server(
350
258
  port=self.redirect_port,
351
259
  server_url=self.server_base_url,
352
- response_future=response_future,
260
+ result_container=result,
261
+ result_ready=result_ready,
353
262
  )
354
263
 
355
264
  # Run server until response is received with timeout logic
@@ -362,13 +271,17 @@ class OAuth(OAuthClientProvider):
362
271
  TIMEOUT = 300.0 # 5 minute timeout
363
272
  try:
364
273
  with anyio.fail_after(TIMEOUT):
365
- auth_code, state = await response_future
366
- return auth_code, state
367
- except TimeoutError:
368
- raise TimeoutError(f"OAuth callback timed out after {TIMEOUT} seconds")
274
+ await result_ready.wait()
275
+ if result.error:
276
+ raise result.error
277
+ return result.code, result.state # type: ignore
278
+ except TimeoutError as e:
279
+ raise TimeoutError(
280
+ f"OAuth callback timed out after {TIMEOUT} seconds"
281
+ ) from e
369
282
  finally:
370
283
  server.should_exit = True
371
- await asyncio.sleep(0.1) # Allow server to shut down gracefully
284
+ await anyio.sleep(0.1) # Allow server to shut down gracefully
372
285
  tg.cancel_scope.cancel()
373
286
 
374
287
  raise RuntimeError("OAuth callback handler could not be started")
@@ -387,7 +300,8 @@ class OAuth(OAuthClientProvider):
387
300
  response = None
388
301
  while True:
389
302
  try:
390
- yielded_request = await gen.asend(response)
303
+ # First iteration sends None, subsequent iterations send response
304
+ yielded_request = await gen.asend(response) # ty: ignore[invalid-argument-type]
391
305
  response = yield yielded_request
392
306
  except StopAsyncIteration:
393
307
  break
@@ -396,32 +310,16 @@ class OAuth(OAuthClientProvider):
396
310
  logger.debug(
397
311
  "OAuth client not found on server, clearing cache and retrying..."
398
312
  )
399
-
400
313
  # Clear cached state and retry once
401
314
  self._initialized = False
315
+ await self.token_storage_adapter.clear()
402
316
 
403
- # Try to clear storage if it supports it
404
- if hasattr(self.context.storage, "clear"):
405
- try:
406
- self.context.storage.clear()
407
- except Exception as e:
408
- logger.warning(f"Failed to clear OAuth storage cache: {e}")
409
- # Can't retry without clearing cache, re-raise original error
410
- raise ClientNotFoundError(
411
- "OAuth client not found and cache could not be cleared"
412
- ) from e
413
- else:
414
- logger.warning(
415
- "Storage does not support clear() - cannot retry with fresh credentials"
416
- )
417
- # Can't retry without clearing cache, re-raise original error
418
- raise
419
-
317
+ # Retry with fresh registration
420
318
  gen = super().async_auth_flow(request)
421
319
  response = None
422
320
  while True:
423
321
  try:
424
- yielded_request = await gen.asend(response)
322
+ yielded_request = await gen.asend(response) # ty: ignore[invalid-argument-type]
425
323
  response = yield yielded_request
426
324
  except StopAsyncIteration:
427
325
  break