fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc2__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 (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1228 -233
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +13 -7
  28. fastmcp/server/auth/providers/aws.py +14 -3
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +14 -8
  32. fastmcp/server/auth/providers/google.py +15 -9
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +17 -14
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +57 -17
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +32 -22
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -12,6 +12,7 @@ This implementation is based on:
12
12
  from collections.abc import Sequence
13
13
 
14
14
  import httpx
15
+ from key_value.aio.protocols import AsyncKeyValue
15
16
  from pydantic import AnyHttpUrl, BaseModel, model_validator
16
17
  from typing_extensions import Self
17
18
 
@@ -19,7 +20,6 @@ from fastmcp.server.auth import TokenVerifier
19
20
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
20
21
  from fastmcp.server.auth.providers.jwt import JWTVerifier
21
22
  from fastmcp.utilities.logging import get_logger
22
- from fastmcp.utilities.storage import KVStorage
23
23
 
24
24
  logger = get_logger(__name__)
25
25
 
@@ -210,10 +210,11 @@ class OIDCProxy(OAuthProxy):
210
210
  required_scopes: list[str] | None = None,
211
211
  # FastMCP server configuration
212
212
  base_url: AnyHttpUrl | str,
213
+ issuer_url: AnyHttpUrl | str | None = None,
213
214
  redirect_path: str | None = None,
214
215
  # Client configuration
215
216
  allowed_client_redirect_uris: list[str] | None = None,
216
- client_storage: KVStorage | None = None,
217
+ client_storage: AsyncKeyValue | None = None,
217
218
  # Token validation configuration
218
219
  token_endpoint_auth_method: str | None = None,
219
220
  ) -> None:
@@ -228,16 +229,16 @@ class OIDCProxy(OAuthProxy):
228
229
  timeout_seconds: HTTP request timeout in seconds
229
230
  algorithm: Token verifier algorithm
230
231
  required_scopes: Required OAuth scopes
231
- base_url: Public URL of the server that exposes this FastMCP server; redirect path is
232
- relative to this URL
232
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
233
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
234
+ to avoid 404s during discovery when mounting under a path.
233
235
  redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
234
236
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
235
237
  Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
236
238
  If None (default), only localhost redirect URIs are allowed.
237
239
  If empty list, all redirect URIs are allowed (not recommended for production).
238
240
  These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
239
- client_storage: Storage implementation for OAuth client registrations.
240
- Defaults to file-based storage if not specified.
241
+ client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
241
242
  token_endpoint_auth_method: Token endpoint authentication method for upstream server.
242
243
  Common values: "client_secret_basic", "client_secret_post", "none".
243
244
  If None, authlib will use its default (typically "client_secret_basic").
@@ -290,6 +291,7 @@ class OIDCProxy(OAuthProxy):
290
291
  "upstream_revocation_endpoint": revocation_endpoint,
291
292
  "token_verifier": token_verifier,
292
293
  "base_url": base_url,
294
+ "issuer_url": issuer_url or base_url,
293
295
  "service_documentation_url": self.oidc_config.service_documentation,
294
296
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
295
297
  "client_storage": client_storage,
@@ -21,13 +21,14 @@ Example:
21
21
  ```
22
22
  """
23
23
 
24
+ from key_value.aio.protocols import AsyncKeyValue
24
25
  from pydantic import AnyHttpUrl, SecretStr, field_validator
25
26
  from pydantic_settings import BaseSettings, SettingsConfigDict
26
27
 
27
28
  from fastmcp.server.auth.oidc_proxy import OIDCProxy
29
+ from fastmcp.settings import ENV_FILE
28
30
  from fastmcp.utilities.auth import parse_scopes
29
31
  from fastmcp.utilities.logging import get_logger
30
- from fastmcp.utilities.storage import KVStorage
31
32
  from fastmcp.utilities.types import NotSet, NotSetT
32
33
 
33
34
  logger = get_logger(__name__)
@@ -38,7 +39,7 @@ class Auth0ProviderSettings(BaseSettings):
38
39
 
39
40
  model_config = SettingsConfigDict(
40
41
  env_prefix="FASTMCP_SERVER_AUTH_AUTH0_",
41
- env_file=".env",
42
+ env_file=ENV_FILE,
42
43
  extra="ignore",
43
44
  )
44
45
 
@@ -47,6 +48,7 @@ class Auth0ProviderSettings(BaseSettings):
47
48
  client_secret: SecretStr | None = None
48
49
  audience: str | None = None
49
50
  base_url: AnyHttpUrl | None = None
51
+ issuer_url: AnyHttpUrl | None = None
50
52
  redirect_path: str | None = None
51
53
  required_scopes: list[str] | None = None
52
54
  allowed_client_redirect_uris: list[str] | None = None
@@ -89,10 +91,11 @@ class Auth0Provider(OIDCProxy):
89
91
  client_secret: str | NotSetT = NotSet,
90
92
  audience: str | NotSetT = NotSet,
91
93
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
94
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
92
95
  required_scopes: list[str] | NotSetT = NotSet,
93
96
  redirect_path: str | NotSetT = NotSet,
94
97
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
95
- client_storage: KVStorage | None = None,
98
+ client_storage: AsyncKeyValue | None = None,
96
99
  ) -> None:
97
100
  """Initialize Auth0 OAuth provider.
98
101
 
@@ -101,13 +104,14 @@ class Auth0Provider(OIDCProxy):
101
104
  client_id: Auth0 application client id
102
105
  client_secret: Auth0 application client secret
103
106
  audience: Auth0 API audience
104
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
107
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
108
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
109
+ to avoid 404s during discovery when mounting under a path.
105
110
  required_scopes: Required Auth0 scopes (defaults to ["openid"])
106
111
  redirect_path: Redirect path configured in Auth0 application
107
112
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
108
113
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
109
- client_storage: Storage implementation for OAuth client registrations.
110
- Defaults to file-based storage if not specified.
114
+ client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
111
115
  """
112
116
  settings = Auth0ProviderSettings.model_validate(
113
117
  {
@@ -118,6 +122,7 @@ class Auth0Provider(OIDCProxy):
118
122
  "client_secret": client_secret,
119
123
  "audience": audience,
120
124
  "base_url": base_url,
125
+ "issuer_url": issuer_url,
121
126
  "required_scopes": required_scopes,
122
127
  "redirect_path": redirect_path,
123
128
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
@@ -159,6 +164,7 @@ class Auth0Provider(OIDCProxy):
159
164
  "client_secret": settings.client_secret.get_secret_value(),
160
165
  "audience": settings.audience,
161
166
  "base_url": settings.base_url,
167
+ "issuer_url": settings.issuer_url,
162
168
  "redirect_path": settings.redirect_path,
163
169
  "required_scopes": auth0_required_scopes,
164
170
  "allowed_client_redirect_uris": settings.allowed_client_redirect_uris,
@@ -167,7 +173,7 @@ class Auth0Provider(OIDCProxy):
167
173
 
168
174
  super().__init__(**init_kwargs)
169
175
 
170
- logger.info(
176
+ logger.debug(
171
177
  "Initialized Auth0 OAuth provider for client %s with scopes: %s",
172
178
  settings.client_id,
173
179
  auth0_required_scopes,
@@ -23,6 +23,7 @@ Example:
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ from key_value.aio.protocols import AsyncKeyValue
26
27
  from pydantic import AnyHttpUrl, SecretStr, field_validator
27
28
  from pydantic_settings import BaseSettings, SettingsConfigDict
28
29
 
@@ -30,6 +31,7 @@ from fastmcp.server.auth import TokenVerifier
30
31
  from fastmcp.server.auth.auth import AccessToken
31
32
  from fastmcp.server.auth.oidc_proxy import OIDCProxy
32
33
  from fastmcp.server.auth.providers.jwt import JWTVerifier
34
+ from fastmcp.settings import ENV_FILE
33
35
  from fastmcp.utilities.auth import parse_scopes
34
36
  from fastmcp.utilities.logging import get_logger
35
37
  from fastmcp.utilities.types import NotSet, NotSetT
@@ -42,7 +44,7 @@ class AWSCognitoProviderSettings(BaseSettings):
42
44
 
43
45
  model_config = SettingsConfigDict(
44
46
  env_prefix="FASTMCP_SERVER_AUTH_AWS_COGNITO_",
45
- env_file=".env",
47
+ env_file=ENV_FILE,
46
48
  extra="ignore",
47
49
  )
48
50
 
@@ -51,6 +53,7 @@ class AWSCognitoProviderSettings(BaseSettings):
51
53
  client_id: str | None = None
52
54
  client_secret: SecretStr | None = None
53
55
  base_url: AnyHttpUrl | str | None = None
56
+ issuer_url: AnyHttpUrl | str | None = None
54
57
  redirect_path: str | None = None
55
58
  required_scopes: list[str] | None = None
56
59
  allowed_client_redirect_uris: list[str] | None = None
@@ -127,9 +130,11 @@ class AWSCognitoProvider(OIDCProxy):
127
130
  client_id: str | NotSetT = NotSet,
128
131
  client_secret: str | NotSetT = NotSet,
129
132
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
133
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
130
134
  redirect_path: str | NotSetT = NotSet,
131
135
  required_scopes: list[str] | NotSetT = NotSet,
132
136
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
137
+ client_storage: AsyncKeyValue | None = None,
133
138
  ):
134
139
  """Initialize AWS Cognito OAuth provider.
135
140
 
@@ -138,11 +143,14 @@ class AWSCognitoProvider(OIDCProxy):
138
143
  aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
139
144
  client_id: Cognito app client ID
140
145
  client_secret: Cognito app client secret
141
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
146
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
147
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
148
+ to avoid 404s during discovery when mounting under a path.
142
149
  redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
143
150
  required_scopes: Required Cognito scopes (defaults to ["openid"])
144
151
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
145
152
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
153
+ client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
146
154
  """
147
155
 
148
156
  settings = AWSCognitoProviderSettings.model_validate(
@@ -154,6 +162,7 @@ class AWSCognitoProvider(OIDCProxy):
154
162
  "client_id": client_id,
155
163
  "client_secret": client_secret,
156
164
  "base_url": base_url,
165
+ "issuer_url": issuer_url,
157
166
  "redirect_path": redirect_path,
158
167
  "required_scopes": required_scopes,
159
168
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
@@ -202,11 +211,13 @@ class AWSCognitoProvider(OIDCProxy):
202
211
  algorithm="RS256",
203
212
  required_scopes=required_scopes_final,
204
213
  base_url=settings.base_url,
214
+ issuer_url=settings.issuer_url,
205
215
  redirect_path=redirect_path_final,
206
216
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
217
+ client_storage=client_storage,
207
218
  )
208
219
 
209
- logger.info(
220
+ logger.debug(
210
221
  "Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
211
222
  settings.client_id,
212
223
  required_scopes_final,
@@ -6,17 +6,23 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import httpx
9
+ from typing import TYPE_CHECKING
10
+
11
+ from key_value.aio.protocols import AsyncKeyValue
10
12
  from pydantic import SecretStr, field_validator
11
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
12
14
 
13
- from fastmcp.server.auth import AccessToken, TokenVerifier
14
15
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
16
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
17
+ from fastmcp.settings import ENV_FILE
15
18
  from fastmcp.utilities.auth import parse_scopes
16
19
  from fastmcp.utilities.logging import get_logger
17
- from fastmcp.utilities.storage import KVStorage
18
20
  from fastmcp.utilities.types import NotSet, NotSetT
19
21
 
22
+ if TYPE_CHECKING:
23
+ from mcp.server.auth.provider import AuthorizationParams
24
+ from mcp.shared.auth import OAuthClientInformationFull
25
+
20
26
  logger = get_logger(__name__)
21
27
 
22
28
 
@@ -25,94 +31,30 @@ class AzureProviderSettings(BaseSettings):
25
31
 
26
32
  model_config = SettingsConfigDict(
27
33
  env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
28
- env_file=".env",
34
+ env_file=ENV_FILE,
29
35
  extra="ignore",
30
36
  )
31
37
 
32
38
  client_id: str | None = None
33
39
  client_secret: SecretStr | None = None
34
40
  tenant_id: str | None = None
41
+ identifier_uri: str | None = None
35
42
  base_url: str | None = None
43
+ issuer_url: str | None = None
36
44
  redirect_path: str | None = None
37
45
  required_scopes: list[str] | None = None
38
- timeout_seconds: int | None = None
46
+ additional_authorize_scopes: list[str] | None = None
39
47
  allowed_client_redirect_uris: list[str] | None = None
40
48
 
41
49
  @field_validator("required_scopes", mode="before")
42
50
  @classmethod
43
- def _parse_scopes(cls, v):
51
+ def _parse_scopes(cls, v: object) -> list[str] | None:
44
52
  return parse_scopes(v)
45
53
 
46
-
47
- class AzureTokenVerifier(TokenVerifier):
48
- """Token verifier for Azure OAuth tokens.
49
-
50
- Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
51
- to get user information and validate the token.
52
- """
53
-
54
- def __init__(
55
- self,
56
- *,
57
- required_scopes: list[str] | None = None,
58
- timeout_seconds: int = 10,
59
- ):
60
- """Initialize the Azure token verifier.
61
-
62
- Args:
63
- required_scopes: Required OAuth scopes
64
- timeout_seconds: HTTP request timeout
65
- """
66
- super().__init__(required_scopes=required_scopes)
67
- self.timeout_seconds = timeout_seconds
68
-
69
- async def verify_token(self, token: str) -> AccessToken | None:
70
- """Verify Azure OAuth token by calling Microsoft Graph API."""
71
- try:
72
- async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
73
- # Use Microsoft Graph API to validate token and get user info
74
- response = await client.get(
75
- "https://graph.microsoft.com/v1.0/me",
76
- headers={
77
- "Authorization": f"Bearer {token}",
78
- "User-Agent": "FastMCP-Azure-OAuth",
79
- },
80
- )
81
-
82
- if response.status_code != 200:
83
- logger.debug(
84
- "Azure token verification failed: %d - %s",
85
- response.status_code,
86
- response.text[:200],
87
- )
88
- return None
89
-
90
- user_data = response.json()
91
-
92
- # Create AccessToken with Azure user info
93
- return AccessToken(
94
- token=token,
95
- client_id=str(user_data.get("id", "unknown")),
96
- scopes=self.required_scopes or [],
97
- expires_at=None,
98
- claims={
99
- "sub": user_data.get("id"),
100
- "email": user_data.get("mail")
101
- or user_data.get("userPrincipalName"),
102
- "name": user_data.get("displayName"),
103
- "given_name": user_data.get("givenName"),
104
- "family_name": user_data.get("surname"),
105
- "job_title": user_data.get("jobTitle"),
106
- "office_location": user_data.get("officeLocation"),
107
- },
108
- )
109
-
110
- except httpx.RequestError as e:
111
- logger.debug("Failed to verify Azure token: %s", e)
112
- return None
113
- except Exception as e:
114
- logger.debug("Azure token verification error: %s", e)
115
- return None
54
+ @field_validator("additional_authorize_scopes", mode="before")
55
+ @classmethod
56
+ def _parse_additional_authorize_scopes(cls, v: object) -> list[str] | None:
57
+ return parse_scopes(v)
116
58
 
117
59
 
118
60
  class AzureProvider(OAuthProxy):
@@ -123,16 +65,17 @@ class AzureProvider(OAuthProxy):
123
65
  Microsoft accounts depending on the tenant configuration.
124
66
 
125
67
  Features:
126
- - Transparent OAuth proxy to Azure/Microsoft identity platform
127
- - Automatic token validation via Microsoft Graph API
128
- - User information extraction
129
- - Support for different tenant configurations (common, organizations, consumers)
130
-
131
- Setup Requirements:
132
- 1. Register an application in Azure Portal (portal.azure.com)
133
- 2. Configure redirect URI as: http://localhost:8000/auth/callback
134
- 3. Note your Application (client) ID and create a client secret
135
- 4. Optionally note your Directory (tenant) ID for single-tenant apps
68
+ - OAuth proxy to Azure/Microsoft identity platform
69
+ - JWT validation using tenant issuer and JWKS
70
+ - Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
71
+
72
+ Setup:
73
+ 1. Create an App registration in Azure Portal
74
+ 2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
75
+ 3. Add an Application ID URI. Either use the default (api://{client_id}) or set a custom one.
76
+ 4. Add a custom scope.
77
+ 5. Create a client secret.
78
+ 6. Get Application (client) ID, Directory (tenant) ID, and client secret
136
79
 
137
80
  Example:
138
81
  ```python
@@ -142,8 +85,10 @@ class AzureProvider(OAuthProxy):
142
85
  auth = AzureProvider(
143
86
  client_id="your-client-id",
144
87
  client_secret="your-client-secret",
145
- tenant_id="your-tenant-id", # Required: your Azure tenant ID from Azure Portal
146
- base_url="http://localhost:8000"
88
+ tenant_id="your-tenant-id",
89
+ required_scopes=["your-scope"],
90
+ base_url="http://localhost:8000",
91
+ # identifier_uri defaults to api://{client_id}
147
92
  )
148
93
 
149
94
  mcp = FastMCP("My App", auth=auth)
@@ -156,27 +101,36 @@ class AzureProvider(OAuthProxy):
156
101
  client_id: str | NotSetT = NotSet,
157
102
  client_secret: str | NotSetT = NotSet,
158
103
  tenant_id: str | NotSetT = NotSet,
104
+ identifier_uri: str | None | NotSetT = NotSet,
159
105
  base_url: str | NotSetT = NotSet,
106
+ issuer_url: str | NotSetT = NotSet,
160
107
  redirect_path: str | NotSetT = NotSet,
161
108
  required_scopes: list[str] | None | NotSetT = NotSet,
162
- timeout_seconds: int | NotSetT = NotSet,
109
+ additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
163
110
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
164
- client_storage: KVStorage | None = None,
165
- ):
111
+ client_storage: AsyncKeyValue | None = None,
112
+ ) -> None:
166
113
  """Initialize Azure OAuth provider.
167
114
 
168
115
  Args:
169
116
  client_id: Azure application (client) ID
170
117
  client_secret: Azure client secret
171
118
  tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
172
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
119
+ identifier_uri: Optional Application ID URI for your API. (defaults to api://{client_id})
120
+ Used only to prefix scopes in authorization requests. Tokens are always validated
121
+ against your app's client ID.
122
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
123
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
124
+ to avoid 404s during discovery when mounting under a path.
173
125
  redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
174
- required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
175
- timeout_seconds: HTTP request timeout for Azure API calls
126
+ required_scopes: Required scopes. These are validated on tokens and used as defaults
127
+ when the client does not request specific scopes.
128
+ additional_authorize_scopes: Additional scopes to include in the authorization request
129
+ without prefixing. Use this to request upstream scopes such as Microsoft Graph
130
+ permissions. These are not used for token validation.
176
131
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
177
132
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
178
- client_storage: Storage implementation for OAuth client registrations.
179
- Defaults to file-based storage if not specified.
133
+ client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
180
134
  """
181
135
  settings = AzureProviderSettings.model_validate(
182
136
  {
@@ -185,10 +139,12 @@ class AzureProvider(OAuthProxy):
185
139
  "client_id": client_id,
186
140
  "client_secret": client_secret,
187
141
  "tenant_id": tenant_id,
142
+ "identifier_uri": identifier_uri,
188
143
  "base_url": base_url,
144
+ "issuer_url": issuer_url,
189
145
  "redirect_path": redirect_path,
190
146
  "required_scopes": required_scopes,
191
- "timeout_seconds": timeout_seconds,
147
+ "additional_authorize_scopes": additional_authorize_scopes,
192
148
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
193
149
  }.items()
194
150
  if v is not NotSet
@@ -197,45 +153,48 @@ class AzureProvider(OAuthProxy):
197
153
 
198
154
  # Validate required settings
199
155
  if not settings.client_id:
200
- raise ValueError(
201
- "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
202
- )
156
+ msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
157
+ raise ValueError(msg)
203
158
  if not settings.client_secret:
204
- raise ValueError(
205
- "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
206
- )
159
+ msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
160
+ raise ValueError(msg)
207
161
 
208
162
  # Validate tenant_id is provided
209
163
  if not settings.tenant_id:
210
- raise ValueError(
211
- "tenant_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. "
212
- "Use your Azure tenant ID (found in Azure Portal), 'organizations', or 'consumers'"
164
+ msg = (
165
+ "tenant_id is required - set via parameter or "
166
+ "FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
167
+ "(found in Azure Portal), 'organizations', or 'consumers'"
213
168
  )
169
+ raise ValueError(msg)
170
+
171
+ if not settings.required_scopes:
172
+ raise ValueError("required_scopes is required")
214
173
 
215
174
  # Apply defaults
175
+ self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
176
+ self.additional_authorize_scopes = settings.additional_authorize_scopes or []
216
177
  tenant_id_final = settings.tenant_id
217
178
 
218
- timeout_seconds_final = settings.timeout_seconds or 10
219
- # Default scopes for Azure - User.Read gives us access to user info via Graph API
220
- scopes_final = settings.required_scopes or [
221
- "User.Read",
222
- "email",
223
- "openid",
224
- "profile",
225
- ]
226
- allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
179
+ # Always validate tokens against the app's API client ID using JWT
180
+ issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
181
+ jwks_uri = (
182
+ f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
183
+ )
184
+
185
+ token_verifier = JWTVerifier(
186
+ jwks_uri=jwks_uri,
187
+ issuer=issuer,
188
+ audience=settings.client_id,
189
+ algorithm="RS256",
190
+ required_scopes=settings.required_scopes,
191
+ )
227
192
 
228
193
  # Extract secret string from SecretStr
229
194
  client_secret_str = (
230
195
  settings.client_secret.get_secret_value() if settings.client_secret else ""
231
196
  )
232
197
 
233
- # Create Azure token verifier
234
- token_verifier = AzureTokenVerifier(
235
- required_scopes=scopes_final,
236
- timeout_seconds=timeout_seconds_final,
237
- )
238
-
239
198
  # Build Azure OAuth endpoints with tenant
240
199
  authorization_endpoint = (
241
200
  f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
@@ -253,13 +212,67 @@ class AzureProvider(OAuthProxy):
253
212
  token_verifier=token_verifier,
254
213
  base_url=settings.base_url,
255
214
  redirect_path=settings.redirect_path,
256
- issuer_url=settings.base_url,
257
- allowed_client_redirect_uris=allowed_client_redirect_uris_final,
215
+ issuer_url=settings.issuer_url
216
+ or settings.base_url, # Default to base_url if not specified
217
+ allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
258
218
  client_storage=client_storage,
259
219
  )
260
220
 
261
221
  logger.info(
262
- "Initialized Azure OAuth provider for client %s with tenant %s",
222
+ "Initialized Azure OAuth provider for client %s with tenant %s%s",
263
223
  settings.client_id,
264
224
  tenant_id_final,
225
+ f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
265
226
  )
227
+
228
+ async def authorize(
229
+ self,
230
+ client: OAuthClientInformationFull,
231
+ params: AuthorizationParams,
232
+ ) -> str:
233
+ """Start OAuth transaction and redirect to Azure AD.
234
+
235
+ Override parent's authorize method to filter out the 'resource' parameter
236
+ which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
237
+ scopes to determine the resource/audience instead of a separate parameter.
238
+
239
+ Args:
240
+ client: OAuth client information
241
+ params: Authorization parameters from the client
242
+
243
+ Returns:
244
+ Authorization URL to redirect the user to Azure AD
245
+ """
246
+ # Clear the resource parameter that Azure AD v2.0 doesn't support
247
+ # This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
248
+ # but Azure AD v2.0 uses scopes instead to determine the audience
249
+ params_to_use = params
250
+ if hasattr(params, "resource"):
251
+ original_resource = getattr(params, "resource", None)
252
+ if original_resource is not None:
253
+ params_to_use = params.model_copy(update={"resource": None})
254
+ if original_resource:
255
+ logger.debug(
256
+ "Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
257
+ original_resource,
258
+ )
259
+ original_scopes = params_to_use.scopes or self.required_scopes
260
+ prefixed_scopes = (
261
+ self._add_prefix_to_scopes(original_scopes)
262
+ if self.identifier_uri
263
+ else original_scopes
264
+ )
265
+
266
+ final_scopes = list(prefixed_scopes)
267
+ if self.additional_authorize_scopes:
268
+ final_scopes.extend(self.additional_authorize_scopes)
269
+
270
+ modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
271
+
272
+ auth_url = await super().authorize(client, modified_params)
273
+ separator = "&" if "?" in auth_url else "?"
274
+ return f"{auth_url}{separator}prompt=select_account"
275
+
276
+ def _add_prefix_to_scopes(self, scopes: list[str]) -> list[str]:
277
+ """Add Application ID URI prefix for authorization request."""
278
+ return [f"{self.identifier_uri}/{scope}" for scope in scopes]
@@ -7,8 +7,6 @@ for seamless MCP client authentication.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any
11
-
12
10
  import httpx
13
11
  from pydantic import AnyHttpUrl
14
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -17,6 +15,7 @@ from starlette.routing import Route
17
15
 
18
16
  from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
19
17
  from fastmcp.server.auth.providers.jwt import JWTVerifier
18
+ from fastmcp.settings import ENV_FILE
20
19
  from fastmcp.utilities.logging import get_logger
21
20
  from fastmcp.utilities.types import NotSet, NotSetT
22
21
 
@@ -26,7 +25,7 @@ logger = get_logger(__name__)
26
25
  class DescopeProviderSettings(BaseSettings):
27
26
  model_config = SettingsConfigDict(
28
27
  env_prefix="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_",
29
- env_file=".env",
28
+ env_file=ENV_FILE,
30
29
  extra="ignore",
31
30
  )
32
31
 
@@ -127,7 +126,6 @@ class DescopeProvider(RemoteAuthProvider):
127
126
  def get_routes(
128
127
  self,
129
128
  mcp_path: str | None = None,
130
- mcp_endpoint: Any | None = None,
131
129
  ) -> list[Route]:
132
130
  """Get OAuth routes including Descope authorization server metadata forwarding.
133
131
 
@@ -136,10 +134,10 @@ class DescopeProvider(RemoteAuthProvider):
136
134
 
137
135
  Args:
138
136
  mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
139
- mcp_endpoint: The MCP endpoint handler to protect with auth
137
+ This is used to advertise the resource URL in metadata.
140
138
  """
141
139
  # Get the standard protected resource routes from RemoteAuthProvider
142
- routes = super().get_routes(mcp_path, mcp_endpoint)
140
+ routes = super().get_routes(mcp_path)
143
141
 
144
142
  async def oauth_authorization_server_metadata(request):
145
143
  """Forward Descope OAuth authorization server metadata with FastMCP customizations."""