mcp-use 1.3.12__py3-none-any.whl → 1.4.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.

Potentially problematic release.


This version of mcp-use might be problematic. Click here for more details.

Files changed (108) hide show
  1. mcp_use/__init__.py +1 -1
  2. mcp_use/adapters/.deprecated +0 -0
  3. mcp_use/adapters/__init__.py +18 -7
  4. mcp_use/adapters/base.py +12 -185
  5. mcp_use/adapters/langchain_adapter.py +12 -219
  6. mcp_use/agents/adapters/__init__.py +17 -0
  7. mcp_use/agents/adapters/anthropic.py +93 -0
  8. mcp_use/agents/adapters/base.py +316 -0
  9. mcp_use/agents/adapters/google.py +103 -0
  10. mcp_use/agents/adapters/langchain_adapter.py +212 -0
  11. mcp_use/agents/adapters/openai.py +111 -0
  12. mcp_use/agents/base.py +1 -1
  13. mcp_use/agents/managers/__init__.py +19 -0
  14. mcp_use/agents/managers/base.py +36 -0
  15. mcp_use/agents/managers/server_manager.py +131 -0
  16. mcp_use/agents/managers/tools/__init__.py +15 -0
  17. mcp_use/agents/managers/tools/base_tool.py +19 -0
  18. mcp_use/agents/managers/tools/connect_server.py +69 -0
  19. mcp_use/agents/managers/tools/disconnect_server.py +43 -0
  20. mcp_use/agents/managers/tools/get_active_server.py +29 -0
  21. mcp_use/agents/managers/tools/list_servers_tool.py +53 -0
  22. mcp_use/agents/managers/tools/search_tools.py +328 -0
  23. mcp_use/agents/mcpagent.py +386 -485
  24. mcp_use/agents/prompts/system_prompt_builder.py +1 -1
  25. mcp_use/agents/remote.py +15 -2
  26. mcp_use/auth/.deprecated +0 -0
  27. mcp_use/auth/__init__.py +19 -4
  28. mcp_use/auth/bearer.py +11 -12
  29. mcp_use/auth/oauth.py +11 -620
  30. mcp_use/auth/oauth_callback.py +16 -207
  31. mcp_use/client/__init__.py +1 -0
  32. mcp_use/client/auth/__init__.py +6 -0
  33. mcp_use/client/auth/bearer.py +23 -0
  34. mcp_use/client/auth/oauth.py +629 -0
  35. mcp_use/client/auth/oauth_callback.py +215 -0
  36. mcp_use/client/client.py +356 -0
  37. mcp_use/client/config.py +106 -0
  38. mcp_use/client/connectors/__init__.py +20 -0
  39. mcp_use/client/connectors/base.py +470 -0
  40. mcp_use/client/connectors/http.py +304 -0
  41. mcp_use/client/connectors/sandbox.py +332 -0
  42. mcp_use/client/connectors/stdio.py +109 -0
  43. mcp_use/client/connectors/utils.py +13 -0
  44. mcp_use/client/connectors/websocket.py +257 -0
  45. mcp_use/client/exceptions.py +31 -0
  46. mcp_use/client/middleware/__init__.py +50 -0
  47. mcp_use/client/middleware/logging.py +31 -0
  48. mcp_use/client/middleware/metrics.py +314 -0
  49. mcp_use/client/middleware/middleware.py +266 -0
  50. mcp_use/client/session.py +162 -0
  51. mcp_use/client/task_managers/__init__.py +20 -0
  52. mcp_use/client/task_managers/base.py +145 -0
  53. mcp_use/client/task_managers/sse.py +84 -0
  54. mcp_use/client/task_managers/stdio.py +69 -0
  55. mcp_use/client/task_managers/streamable_http.py +86 -0
  56. mcp_use/client/task_managers/websocket.py +68 -0
  57. mcp_use/client.py +12 -344
  58. mcp_use/config.py +20 -97
  59. mcp_use/connectors/.deprecated +0 -0
  60. mcp_use/connectors/__init__.py +46 -20
  61. mcp_use/connectors/base.py +12 -455
  62. mcp_use/connectors/http.py +13 -300
  63. mcp_use/connectors/sandbox.py +13 -306
  64. mcp_use/connectors/stdio.py +13 -104
  65. mcp_use/connectors/utils.py +15 -8
  66. mcp_use/connectors/websocket.py +13 -252
  67. mcp_use/exceptions.py +33 -18
  68. mcp_use/logging.py +1 -1
  69. mcp_use/managers/.deprecated +0 -0
  70. mcp_use/managers/__init__.py +56 -17
  71. mcp_use/managers/base.py +13 -31
  72. mcp_use/managers/server_manager.py +13 -119
  73. mcp_use/managers/tools/__init__.py +45 -15
  74. mcp_use/managers/tools/base_tool.py +5 -16
  75. mcp_use/managers/tools/connect_server.py +5 -67
  76. mcp_use/managers/tools/disconnect_server.py +5 -41
  77. mcp_use/managers/tools/get_active_server.py +5 -26
  78. mcp_use/managers/tools/list_servers_tool.py +5 -51
  79. mcp_use/managers/tools/search_tools.py +17 -321
  80. mcp_use/middleware/.deprecated +0 -0
  81. mcp_use/middleware/__init__.py +89 -50
  82. mcp_use/middleware/logging.py +14 -26
  83. mcp_use/middleware/metrics.py +30 -303
  84. mcp_use/middleware/middleware.py +39 -246
  85. mcp_use/session.py +13 -149
  86. mcp_use/task_managers/.deprecated +0 -0
  87. mcp_use/task_managers/__init__.py +48 -20
  88. mcp_use/task_managers/base.py +13 -140
  89. mcp_use/task_managers/sse.py +13 -79
  90. mcp_use/task_managers/stdio.py +13 -64
  91. mcp_use/task_managers/streamable_http.py +15 -81
  92. mcp_use/task_managers/websocket.py +13 -63
  93. mcp_use/telemetry/events.py +58 -0
  94. mcp_use/telemetry/telemetry.py +71 -1
  95. mcp_use/telemetry/utils.py +1 -1
  96. mcp_use/types/.deprecated +0 -0
  97. mcp_use/types/sandbox.py +13 -18
  98. {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/METADATA +68 -43
  99. mcp_use-1.4.0.dist-info/RECORD +111 -0
  100. mcp_use/cli.py +0 -581
  101. mcp_use-1.3.12.dist-info/RECORD +0 -64
  102. mcp_use-1.3.12.dist-info/licenses/LICENSE +0 -21
  103. /mcp_use/{observability → agents/observability}/__init__.py +0 -0
  104. /mcp_use/{observability → agents/observability}/callbacks_manager.py +0 -0
  105. /mcp_use/{observability → agents/observability}/laminar.py +0 -0
  106. /mcp_use/{observability → agents/observability}/langfuse.py +0 -0
  107. {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/WHEEL +0 -0
  108. {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,629 @@
1
+ """OAuth authentication support for MCP clients."""
2
+
3
+ import json
4
+ import secrets
5
+ import webbrowser
6
+ from datetime import UTC, datetime, timedelta
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+ import httpx
12
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
13
+ from authlib.oauth2 import OAuth2Error
14
+ from pydantic import BaseModel, Field, HttpUrl, SecretStr
15
+
16
+ from mcp_use.client.auth.bearer import BearerAuth
17
+ from mcp_use.client.auth.oauth_callback import OAuthCallbackServer
18
+ from mcp_use.client.exceptions import OAuthAuthenticationError, OAuthDiscoveryError
19
+ from mcp_use.logging import logger
20
+ from mcp_use.telemetry.telemetry import telemetry
21
+
22
+
23
+ class ServerOAuthMetadata(BaseModel):
24
+ """OAuth metadata from MCP server with flexible field support.
25
+ It is essentially a configuration that tells MCP client:
26
+
27
+ - Where to send users for authorization
28
+ - Where to exchange the codes for tokens
29
+ - Which OAuth features are supported
30
+ - Where to register new users with DCR"""
31
+
32
+ issuer: HttpUrl # The OAuth server's identity
33
+ authorization_endpoint: HttpUrl # URL with endpoint for client auth
34
+ token_endpoint: HttpUrl # URL with endpoint for tokens' exchange
35
+ userinfo_endpoint: HttpUrl | None = None
36
+ revocation_endpoint: HttpUrl | None = None
37
+ introspection_endpoint: HttpUrl | None = None
38
+ registration_endpoint: HttpUrl | None = None # Endpoint for DCR
39
+ jwks_uri: HttpUrl | None = None
40
+ response_types_supported: list[str] = Field(default_factory=lambda: ["code"])
41
+ subject_types_supported: list[str] = Field(default_factory=lambda: ["public"])
42
+ id_token_signing_alg_values_supported: list[str] = Field(default_factory=lambda: ["RS256"])
43
+ scopes_supported: list[str] | None = None # Which permissions are supported
44
+ token_endpoint_auth_methods_supported: list[str] = Field(default_factory=lambda: ["client_secret_basic"])
45
+ claims_supported: list[str] | None = None
46
+ code_challenge_methods_supported: list[str] | None = None
47
+
48
+ class Config:
49
+ extra = "allow" # Allow additional fields
50
+
51
+
52
+ class OAuthClientProvider(BaseModel):
53
+ """OAuth client provider configuration for a specific server.
54
+
55
+ This contains all the information needed to authenticate with an OAuth server
56
+ without needing to discover metadata or register clients dynamically."""
57
+
58
+ id: str # Unique identifier
59
+ display_name: str
60
+ metadata: ServerOAuthMetadata | dict[str, Any]
61
+
62
+ @property
63
+ def oauth_metadata(self) -> ServerOAuthMetadata:
64
+ """Get OAuth metadata as ServerOAuthMetadata instance."""
65
+ if isinstance(self.metadata, dict):
66
+ return ServerOAuthMetadata(**self.metadata)
67
+ return self.metadata
68
+
69
+
70
+ class TokenData(BaseModel):
71
+ """OAuth token data.
72
+
73
+ This is the information received after
74
+ successfull authentication"""
75
+
76
+ access_token: str # Actual credential used for requests
77
+ token_type: str = "Bearer"
78
+ expires_at: float | None = None
79
+ refresh_token: str | None = None
80
+ scope: str | None = None
81
+
82
+
83
+ class ClientRegistrationResponse(BaseModel):
84
+ """Dynamic Client Registration response.
85
+
86
+ It represents the response from an OAuth server
87
+ when you dinamically register a new OAuth client."""
88
+
89
+ client_id: str
90
+ client_secret: str | None = None
91
+ client_id_issued_at: int | None = None
92
+ client_secret_expires_at: int | None = None
93
+ redirect_uris: list[str] | None = None # Where auth server should redirect after auth
94
+ grant_types: list[str] | None = None # Which oauth flows it uses
95
+ response_types: list[str] | None = None
96
+ client_name: str | None = None
97
+ token_endpoint_auth_method: str | None = None
98
+
99
+ class Config:
100
+ extra = "allow" # Allow additional fields from server
101
+
102
+
103
+ class FileTokenStorage:
104
+ """File-based token storage.
105
+
106
+ It's responsible for:
107
+
108
+ - Saving OAuth tokens to disk after auth
109
+ - Loading saved tokens when the app restarts
110
+ - Deleting tokens when they're revoked
111
+ - Organizing tokens by server URL"""
112
+
113
+ def __init__(self, base_dir: Path | None = None):
114
+ """Initialize token storage.
115
+
116
+ Args:
117
+ base_dir: Base directory for token storage. Defaults to ~/.mcp_use/tokens
118
+ """
119
+ self.base_dir = base_dir or Path.home() / ".mcp_use" / "tokens"
120
+ logger.debug(f"FileTokenStorage initialized with base_dir: {self.base_dir}")
121
+ self.base_dir.mkdir(parents=True, exist_ok=True)
122
+
123
+ def _get_token_path(self, server_url: str) -> Path:
124
+ """Get token file path for a server."""
125
+ # Create a safe filename from the URL
126
+ parsed = urlparse(server_url)
127
+ filename = f"{parsed.netloc}_{parsed.path.replace('/', '_')}.json"
128
+ path = self.base_dir / filename
129
+ logger.debug(f"Token path for server '{server_url}' is '{path}'")
130
+ return path
131
+
132
+ async def save_tokens(self, server_url: str, tokens: dict[str, Any]) -> None:
133
+ """Save tokens to file."""
134
+ token_path = self._get_token_path(server_url)
135
+ logger.debug(f"Saving tokens for '{server_url}' to '{token_path}'")
136
+ token_data = TokenData(**tokens)
137
+ token_path.write_text(token_data.model_dump_json())
138
+ logger.debug(f"Tokens saved successfully for '{server_url}'")
139
+
140
+ async def load_tokens(self, server_url: str) -> TokenData | None:
141
+ """Load tokens from file."""
142
+ token_path = self._get_token_path(server_url)
143
+ logger.debug(f"Attempting to load tokens for '{server_url}' from '{token_path}'")
144
+ if not token_path.exists():
145
+ logger.debug(f"Token file not found: '{token_path}'")
146
+ return None
147
+
148
+ try:
149
+ data = json.loads(token_path.read_text())
150
+ token_data = TokenData(**data)
151
+ logger.debug(f"Successfully loaded tokens for '{server_url}'")
152
+ return token_data
153
+ except (json.JSONDecodeError, ValueError) as e:
154
+ logger.debug(f"Failed to load or parse token file '{token_path}': {e}")
155
+ return None
156
+
157
+ async def delete_tokens(self, server_url: str) -> None:
158
+ """Delete tokens for a server."""
159
+ token_path = self._get_token_path(server_url)
160
+ logger.debug(f"Deleting tokens for '{server_url}' at '{token_path}'")
161
+ if token_path.exists():
162
+ token_path.unlink()
163
+ logger.debug(f"Token file '{token_path}' deleted.")
164
+ else:
165
+ logger.debug(f"Token file '{token_path}' not found, nothing to delete.")
166
+
167
+
168
+ class OAuth:
169
+ """OAuth authentication handler for MCP clients.
170
+
171
+ This is the main class that handles all the authentication
172
+ It has several features:
173
+
174
+ - Discovers OAuth server capabilities automatically
175
+ - Registers client dynamically when possible
176
+ - Manages token storage and refresh automaticlly"""
177
+
178
+ def __init__(
179
+ self,
180
+ server_url: str,
181
+ token_storage: FileTokenStorage | None = None,
182
+ scope: str | None = None,
183
+ client_id: str | None = None,
184
+ client_secret: str | None = None,
185
+ callback_port: int | None = None,
186
+ oauth_provider: OAuthClientProvider | None = None,
187
+ ):
188
+ """Initialize OAuth handler.
189
+
190
+ Args:
191
+ server_url: The MCP server URL
192
+ token_storage: Token storage implementation. Defaults to FileTokenStorage
193
+ scope: OAuth scopes to request
194
+ client_id: OAuth client ID. If not provided, will attempt dynamic registration
195
+ client_secret: OAuth client secret (for confidential clients)
196
+ callback_port: Port for local callback server, if empty, 8080 is used
197
+ oauth_provider: OAuth client provider to prevent metadata discovery
198
+ """
199
+ logger.debug(f"Initializing OAuth for server: {server_url}")
200
+ self.server_url = server_url
201
+ self.token_storage = token_storage or FileTokenStorage()
202
+ self.scope = scope
203
+ self.client_id = client_id
204
+ self.client_secret = client_secret
205
+
206
+ if callback_port:
207
+ self.callback_port = callback_port
208
+ logger.info(f"Using custom callback port {self.callback_port} provided in config")
209
+ else:
210
+ self.callback_port = 8080
211
+ logger.info(f"Using default callback port {self.callback_port}")
212
+
213
+ # Set the default redirect uri
214
+ self.redirect_uri = f"http://localhost:{self.callback_port}/callback"
215
+ self._oauth_provider = oauth_provider
216
+ self._metadata: ServerOAuthMetadata | None = None
217
+
218
+ if self._oauth_provider:
219
+ self._metadata = self._oauth_provider.oauth_metadata
220
+ logger.debug(f"Using OAuth provider {self._oauth_provider.id} with metadata")
221
+
222
+ self._client: AsyncOAuth2Client | None = None
223
+ self._bearer_auth: BearerAuth | None = None
224
+ logger.debug(f"OAuth initialized with scope='{self.scope}', client_id='{self.client_id}'")
225
+
226
+ @telemetry("oauth_initialize")
227
+ async def initialize(self, client: httpx.AsyncClient) -> BearerAuth | None:
228
+ """Initialize OAuth and return bearer auth if tokens exist."""
229
+ logger.debug(f"OAuth.initialize called for {self.server_url}")
230
+ # Try to load existing tokens
231
+ logger.debug("Attempting to load existing tokens")
232
+ token_data = await self.token_storage.load_tokens(self.server_url)
233
+ if token_data:
234
+ logger.debug("Found existing tokens, checking validity")
235
+ if self._is_token_valid(token_data):
236
+ logger.debug("Existing token is valid, creating BearerAuth")
237
+ self._bearer_auth = BearerAuth(token=SecretStr(token_data.access_token))
238
+ logger.debug("OAuth.initialize returning existing valid BearerAuth")
239
+ return self._bearer_auth
240
+ else:
241
+ logger.debug("Existing token is expired")
242
+ else:
243
+ logger.debug("No existing tokens found")
244
+
245
+ # Discover OAuth metadata
246
+ if not self._metadata:
247
+ logger.debug("No valid token, proceeding to discover OAuth metadata")
248
+ await self._discover_metadata(client)
249
+ else:
250
+ logger.debug("Using provided OAuth metadata, skipping discovery")
251
+
252
+ logger.debug("OAuth.initialize finished, no valid token available yet")
253
+ return None
254
+
255
+ @telemetry("oauth_authenticate")
256
+ async def authenticate(self) -> BearerAuth:
257
+ """Perform OAuth authentication flow."""
258
+ logger.debug("OAuth.authenticate called")
259
+ if not self._metadata:
260
+ logger.error("OAuth.authenticate called before metadata was discovered.")
261
+ raise OAuthAuthenticationError("OAuth metadata not discovered")
262
+
263
+ # The port check should be done now. OAuth servers
264
+ # register client_id with also redirect_uri, so we
265
+ # have to ensure port is available before DCR
266
+ try:
267
+ import socket
268
+
269
+ sock = socket.socket()
270
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
271
+ sock.bind(("127.0.0.1", self.callback_port))
272
+ sock.close()
273
+ logger.debug(f"Using registered port {self.callback_port} for callback")
274
+ except (ValueError, OSError) as exception:
275
+ logger.error(f"The port {self.callback_port} is not available! Try using a different port!")
276
+ raise exception
277
+
278
+ # Try to get client_id - either from config or dynamic registration
279
+ client_id = self.client_id
280
+ client_secret = self.client_secret
281
+ registration = None # Track if we used DCR
282
+
283
+ if not client_id:
284
+ logger.debug("No client_id provided, attempting dynamic client registration")
285
+ # Try to load previously registered client
286
+ registration = await self._load_client_registration()
287
+
288
+ if registration:
289
+ logger.debug("Using previously registered client")
290
+ client_id = registration.client_id
291
+ client_secret = registration.client_secret
292
+ else:
293
+ # Attempt dynamic registration
294
+ registration = await self._try_dynamic_registration()
295
+ if registration:
296
+ logger.debug("Dynamic registration successful")
297
+ client_id = registration.client_id
298
+ client_secret = registration.client_secret
299
+ # Store for future use
300
+ await self._store_client_registration(registration)
301
+ else:
302
+ logger.error("Dynamic client registration failed or not supported")
303
+ raise OAuthAuthenticationError(
304
+ "OAuth requires a client_id. Server does not support dynamic registration. "
305
+ "Please provide one in the auth configuration. "
306
+ "Example: {'auth': {'client_id': 'your-registered-client-id'}}"
307
+ )
308
+
309
+ logger.debug(f"Using client_id: {client_id}")
310
+
311
+ # Create OAuth client
312
+ logger.debug("Creating AsyncOAuth2Client")
313
+ self._client = AsyncOAuth2Client(
314
+ client_id=client_id,
315
+ client_secret=client_secret,
316
+ redirect_uri=self.redirect_uri,
317
+ scope=self.scope,
318
+ )
319
+
320
+ # Start callback server
321
+ logger.debug("Starting OAuth callback server")
322
+
323
+ callback_server = OAuthCallbackServer(port=self.callback_port)
324
+ redirect_uri = await callback_server.start()
325
+ self._client.redirect_uri = redirect_uri
326
+ logger.debug(f"Callback server started, redirect_uri: {redirect_uri}")
327
+
328
+ # Generate state for CSRF protection
329
+ state = secrets.token_urlsafe(32)
330
+ logger.debug(f"Generated state for CSRF protection: {state}")
331
+
332
+ # Build authorization URL
333
+ logger.debug("Creating authorization URL")
334
+ auth_url, _ = self._client.create_authorization_url(
335
+ str(self._metadata.authorization_endpoint),
336
+ state=state,
337
+ )
338
+
339
+ logger.debug("OAuth flow started:")
340
+ logger.debug(f" Client ID: {client_id}")
341
+ logger.debug(f" Authorization endpoint: {self._metadata.authorization_endpoint}")
342
+ logger.debug(f" Redirect URI: {redirect_uri}")
343
+ logger.debug(f" Scope: {self.scope}")
344
+
345
+ # Open browser for authorization
346
+ print(f"Opening browser for authorization: {auth_url}")
347
+ webbrowser.open(auth_url)
348
+
349
+ # Wait for callback
350
+ logger.debug("Waiting for authorization code from callback server")
351
+ try:
352
+ response = await callback_server.wait_for_code()
353
+ logger.debug("Received response from callback server")
354
+ except TimeoutError as e:
355
+ logger.error(f"OAuth callback timed out: {e}")
356
+ raise OAuthAuthenticationError(f"OAuth timeout: {e}") from e
357
+
358
+ if response.error:
359
+ logger.error("OAuth authorization failed:")
360
+ logger.error(f" Error: {response.error}")
361
+ logger.error(f" Description: {response.error_description}")
362
+ logger.error(" The OAuth server returned this error, likely because:")
363
+ logger.error(f" 1. The client_id '{client_id}' is not registered with the OAuth server")
364
+ logger.error(" 2. The redirect_uri doesn't match the registered one")
365
+ logger.error(" 3. The requested scopes are invalid")
366
+ raise OAuthAuthenticationError(f"{response.error}: {response.error_description}")
367
+
368
+ if not response.code:
369
+ logger.error("Callback response did not contain an authorization code")
370
+ raise OAuthAuthenticationError("No authorization code received")
371
+
372
+ logger.debug(f"Received authorization code: {response.code[:10]}...")
373
+
374
+ # Verify state
375
+ logger.debug(f"Verifying state. Expected: {state}, Got: {response.state}")
376
+ if response.state != state:
377
+ logger.error("State mismatch in OAuth callback. Possible CSRF attack.")
378
+ raise OAuthAuthenticationError("Invalid state parameter - possible CSRF attack")
379
+ logger.debug("State verified successfully")
380
+
381
+ # Exchange code for tokens
382
+ logger.debug("Exchanging authorization code for tokens")
383
+ try:
384
+ token_response = await self._client.fetch_token(
385
+ str(self._metadata.token_endpoint),
386
+ authorization_response=f"{redirect_uri}?code={response.code}&state={response.state}",
387
+ grant_type="authorization_code",
388
+ )
389
+ logger.debug("Successfully fetched tokens")
390
+ except OAuth2Error as e:
391
+ logger.error(f"Token exchange failed: {e}")
392
+ raise OAuthAuthenticationError(f"Token exchange failed: {e}") from e
393
+
394
+ # Save tokens
395
+ logger.debug("Saving fetched tokens")
396
+ await self.token_storage.save_tokens(self.server_url, token_response)
397
+
398
+ # Create bearer auth
399
+ logger.debug("Creating BearerAuth with new access token")
400
+ self._bearer_auth = BearerAuth(token=SecretStr(token_response["access_token"]))
401
+ return self._bearer_auth
402
+
403
+ async def _discover_metadata(self, client: httpx.AsyncClient) -> None:
404
+ """Discover OAuth metadata from server."""
405
+ logger.debug(f"Discovering OAuth metadata for {self.server_url}")
406
+ # Try well-known endpoint first
407
+ parsed = urlparse(self.server_url)
408
+
409
+ # Edge case for GH that doesn't have metadata discovery
410
+ if parsed.netloc == "api.githubcopilot.com":
411
+ logger.debug("Detected GitHub MCP server, using its metadata")
412
+ issuer = "https://github.com/login/oauth"
413
+ authorization_endpoint = "https://github.com/login/oauth/authorize"
414
+ token_endpoint = "https://github.com/login/oauth/access_token"
415
+ self._metadata = ServerOAuthMetadata(
416
+ issuer=issuer, authorization_endpoint=authorization_endpoint, token_endpoint=token_endpoint
417
+ )
418
+ return
419
+
420
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
421
+ well_known_url = f"{base_url}/.well-known/oauth-authorization-server"
422
+
423
+ try:
424
+ logger.debug(f"Trying OAuth metadata discovery at: {well_known_url}")
425
+ response = await client.get(well_known_url)
426
+ response.raise_for_status()
427
+ metadata = response.json()
428
+ self._metadata = ServerOAuthMetadata(**metadata)
429
+ logger.debug("Successfully discovered OAuth metadata")
430
+ logger.debug(f" Authorization endpoint: {self._metadata.authorization_endpoint}")
431
+ logger.debug(f" Token endpoint: {self._metadata.token_endpoint}")
432
+ return
433
+ except (httpx.HTTPError, ValueError) as e:
434
+ logger.debug(f"Failed to discover OAuth metadata at {well_known_url}: {e}")
435
+ pass
436
+
437
+ # Try OpenID Connect discovery
438
+ oidc_url = f"{base_url}/.well-known/openid-configuration"
439
+ logger.debug(f"Trying OpenID Connect discovery at: {oidc_url}")
440
+ try:
441
+ response = await client.get(oidc_url)
442
+ response.raise_for_status()
443
+ metadata = response.json()
444
+ self._metadata = ServerOAuthMetadata(**metadata)
445
+ logger.debug("Successfully discovered OIDC metadata")
446
+ logger.debug(f" Authorization endpoint: {self._metadata.authorization_endpoint}")
447
+ logger.debug(f" Token endpoint: {self._metadata.token_endpoint}")
448
+ return
449
+ except (httpx.HTTPError, ValueError) as e:
450
+ logger.debug(f"Failed to discover OIDC metadata at {oidc_url}: {e}")
451
+ pass
452
+
453
+ # If discovery fails, we'll need the metadata from somewhere else
454
+ logger.error(f"Failed to discover OAuth/OIDC metadata for {self.server_url}")
455
+ raise OAuthDiscoveryError(
456
+ f"Failed to discover OAuth metadata for {self.server_url}. "
457
+ "Server must support OAuth metadata discovery at "
458
+ "/.well-known/oauth-authorization-server or /.well-known/openid-configuration"
459
+ )
460
+
461
+ def _is_token_valid(self, token_data: TokenData) -> bool:
462
+ """Check if token is still valid."""
463
+ logger.debug("Checking token validity")
464
+ if not token_data.expires_at:
465
+ logger.debug("Token has no expiration time, assuming it's valid.")
466
+ return True # No expiration info, assume valid
467
+
468
+ # Check if token expires in more than 60 seconds
469
+ expires_at = datetime.fromtimestamp(token_data.expires_at, tz=UTC)
470
+ now = datetime.now(tz=UTC)
471
+ is_valid = expires_at > now + timedelta(seconds=60)
472
+ logger.debug(f"Token expires at {expires_at}, current time is {now}. Valid: {is_valid}")
473
+ return is_valid
474
+
475
+ async def _try_dynamic_registration(self) -> ClientRegistrationResponse | None:
476
+ """Try Dynamic Client Registration if supported by the server."""
477
+ if not self._metadata or not self._metadata.registration_endpoint:
478
+ logger.debug("No registration endpoint available, skipping DCR")
479
+ return None
480
+
481
+ logger.info("Attempting Dynamic Client Registration")
482
+ logger.debug(f"DCR endpoint: {self._metadata.registration_endpoint}")
483
+
484
+ registration_data = {
485
+ "client_name": "mcp-use",
486
+ "redirect_uris": [self.redirect_uri],
487
+ "grant_types": ["authorization_code"],
488
+ "response_types": ["code"],
489
+ "token_endpoint_auth_method": "none", # Public client
490
+ "application_type": "native",
491
+ }
492
+
493
+ # Add scope if specified
494
+ if self.scope:
495
+ registration_data["scope"] = self.scope
496
+
497
+ logger.debug(f"DCR request payload: {registration_data}")
498
+ try:
499
+ async with httpx.AsyncClient() as client:
500
+ response = await client.post(
501
+ str(self._metadata.registration_endpoint),
502
+ json=registration_data,
503
+ headers={"Content-Type": "application/json"},
504
+ )
505
+ logger.debug(f"DCR response status: {response.status_code}")
506
+ response.raise_for_status()
507
+
508
+ # Parse registration response
509
+ reg_response_data = response.json()
510
+ logger.debug(f"DCR response body: {reg_response_data}")
511
+ reg_response = ClientRegistrationResponse(**reg_response_data)
512
+
513
+ # Update our credentials
514
+ self.client_id = reg_response.client_id
515
+ self.client_secret = reg_response.client_secret
516
+
517
+ logger.info(f"Dynamic Client Registration successful: {self.client_id}")
518
+
519
+ # Store the registered client info for future use
520
+ await self._store_client_registration(reg_response)
521
+
522
+ return reg_response
523
+
524
+ except httpx.HTTPError as e:
525
+ logger.warning(f"Dynamic Client Registration failed: {e}")
526
+ # Log the response if available
527
+ if hasattr(e, "response") and e.response:
528
+ logger.debug(f"DCR response: {e.response.status_code} - {e.response.text}")
529
+ return None
530
+ except Exception as e:
531
+ logger.warning(f"Unexpected error during DCR: {e}")
532
+ return None
533
+
534
+ async def _store_client_registration(self, registration: ClientRegistrationResponse) -> None:
535
+ """Store client registration data for future use."""
536
+ logger.debug("Storing client registration data")
537
+ # Store alongside tokens in a separate file
538
+ storage_path = self.token_storage.base_dir / "registrations"
539
+ storage_path.mkdir(parents=True, exist_ok=True)
540
+
541
+ # Create a safe filename from the server URL
542
+ parsed = urlparse(self.server_url)
543
+ filename = f"{parsed.netloc}_{parsed.path.replace('/', '_')}_registration.json"
544
+ reg_path = storage_path / filename
545
+ logger.debug(f"Storing client registration to '{reg_path}'")
546
+
547
+ # Store registration data
548
+ reg_path.write_text(registration.model_dump_json())
549
+ logger.debug("Client registration data stored successfully")
550
+
551
+ async def _load_client_registration(self) -> ClientRegistrationResponse | None:
552
+ """Load previously registered client credentials if available."""
553
+ logger.debug("Attempting to load client registration data")
554
+ storage_path = self.token_storage.base_dir / "registrations"
555
+
556
+ # Create a safe filename from the server URL
557
+ parsed = urlparse(self.server_url)
558
+ filename = f"{parsed.netloc}_{parsed.path.replace('/', '_')}_registration.json"
559
+ reg_path = storage_path / filename
560
+ logger.debug(f"Checking for client registration file at '{reg_path}'")
561
+
562
+ if reg_path.exists():
563
+ logger.debug("Client registration file found")
564
+ try:
565
+ data = json.loads(reg_path.read_text())
566
+ reg_response = ClientRegistrationResponse(**data)
567
+
568
+ # Check if registration is still valid (if expiry info provided)
569
+ if reg_response.client_secret_expires_at:
570
+ expires_at = datetime.fromtimestamp(reg_response.client_secret_expires_at, tz=UTC)
571
+ now = datetime.now(tz=UTC)
572
+ logger.debug(f"Checking client registration expiry. Expires at: {expires_at}, Now: {now}")
573
+ if expires_at <= now:
574
+ logger.debug("Stored client registration has expired")
575
+ return None
576
+
577
+ self.client_id = reg_response.client_id
578
+ self.client_secret = reg_response.client_secret
579
+ logger.debug(f"Loaded stored client registration: {self.client_id}")
580
+ return reg_response
581
+
582
+ except Exception as e:
583
+ logger.debug(f"Failed to load client registration: {e}")
584
+ else:
585
+ logger.debug("Client registration file not found")
586
+
587
+ return None
588
+
589
+ @telemetry("oauth_refresh_token")
590
+ async def refresh_token(self) -> BearerAuth | None:
591
+ """Refresh the access token if possible."""
592
+ logger.debug("Attempting to refresh token")
593
+ token_data = await self.token_storage.load_tokens(self.server_url)
594
+ if not token_data or not token_data.refresh_token:
595
+ logger.debug("No token data or refresh token found, cannot refresh.")
596
+ return None
597
+
598
+ if not self._metadata:
599
+ logger.debug("No OAuth metadata available, cannot refresh token.")
600
+ return None
601
+
602
+ if not self._client:
603
+ if not self.client_id:
604
+ logger.debug("Cannot refresh token without client_id")
605
+ return None
606
+ logger.debug("Creating temporary AsyncOAuth2Client for token refresh")
607
+ self._client = AsyncOAuth2Client(client_id=self.client_id, client_secret=self.client_secret)
608
+
609
+ logger.debug("Calling client.refresh_token")
610
+ try:
611
+ token_response = await self._client.refresh_token(
612
+ str(self._metadata.token_endpoint),
613
+ refresh_token=token_data.refresh_token,
614
+ )
615
+ logger.debug("Token refresh successful")
616
+
617
+ # Save new tokens
618
+ logger.debug("Saving new tokens after refresh")
619
+ await self.token_storage.save_tokens(self.server_url, token_response)
620
+
621
+ # Update bearer auth
622
+ logger.debug("Updating BearerAuth with new access token")
623
+ self._bearer_auth = BearerAuth(token=SecretStr(token_response["access_token"]))
624
+ return self._bearer_auth
625
+
626
+ except OAuth2Error as e:
627
+ logger.warning(f"Token refresh failed: {e}. Re-authentication is required.")
628
+ # Refresh failed, need to re-authenticate
629
+ return None