fastmcp 2.10.6__py3-none-any.whl → 2.11.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.
Files changed (61) hide show
  1. fastmcp/cli/cli.py +128 -33
  2. fastmcp/cli/install/claude_code.py +42 -1
  3. fastmcp/cli/install/claude_desktop.py +42 -1
  4. fastmcp/cli/install/cursor.py +42 -1
  5. fastmcp/cli/install/mcp_json.py +41 -0
  6. fastmcp/cli/run.py +127 -1
  7. fastmcp/client/__init__.py +2 -0
  8. fastmcp/client/auth/oauth.py +68 -99
  9. fastmcp/client/oauth_callback.py +18 -0
  10. fastmcp/client/transports.py +69 -15
  11. fastmcp/contrib/component_manager/example.py +2 -2
  12. fastmcp/experimental/server/openapi/README.md +266 -0
  13. fastmcp/experimental/server/openapi/__init__.py +38 -0
  14. fastmcp/experimental/server/openapi/components.py +348 -0
  15. fastmcp/experimental/server/openapi/routing.py +132 -0
  16. fastmcp/experimental/server/openapi/server.py +466 -0
  17. fastmcp/experimental/utilities/openapi/README.md +239 -0
  18. fastmcp/experimental/utilities/openapi/__init__.py +68 -0
  19. fastmcp/experimental/utilities/openapi/director.py +208 -0
  20. fastmcp/experimental/utilities/openapi/formatters.py +355 -0
  21. fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
  22. fastmcp/experimental/utilities/openapi/models.py +85 -0
  23. fastmcp/experimental/utilities/openapi/parser.py +618 -0
  24. fastmcp/experimental/utilities/openapi/schemas.py +538 -0
  25. fastmcp/mcp_config.py +125 -88
  26. fastmcp/prompts/prompt.py +11 -1
  27. fastmcp/resources/resource.py +21 -1
  28. fastmcp/resources/template.py +20 -1
  29. fastmcp/server/auth/__init__.py +17 -2
  30. fastmcp/server/auth/auth.py +144 -7
  31. fastmcp/server/auth/providers/bearer.py +25 -473
  32. fastmcp/server/auth/providers/in_memory.py +4 -2
  33. fastmcp/server/auth/providers/jwt.py +538 -0
  34. fastmcp/server/auth/providers/workos.py +170 -0
  35. fastmcp/server/auth/registry.py +52 -0
  36. fastmcp/server/context.py +107 -26
  37. fastmcp/server/dependencies.py +9 -2
  38. fastmcp/server/http.py +62 -30
  39. fastmcp/server/middleware/middleware.py +3 -23
  40. fastmcp/server/openapi.py +1 -1
  41. fastmcp/server/proxy.py +50 -11
  42. fastmcp/server/server.py +168 -59
  43. fastmcp/settings.py +73 -6
  44. fastmcp/tools/tool.py +36 -3
  45. fastmcp/tools/tool_manager.py +38 -2
  46. fastmcp/tools/tool_transform.py +112 -3
  47. fastmcp/utilities/components.py +35 -2
  48. fastmcp/utilities/json_schema.py +136 -98
  49. fastmcp/utilities/json_schema_type.py +1 -3
  50. fastmcp/utilities/mcp_config.py +28 -0
  51. fastmcp/utilities/openapi.py +240 -50
  52. fastmcp/utilities/tests.py +54 -6
  53. fastmcp/utilities/types.py +89 -11
  54. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
  55. fastmcp-2.11.0.dist-info/RECORD +108 -0
  56. fastmcp/server/auth/providers/bearer_env.py +0 -63
  57. fastmcp/utilities/cache.py +0 -26
  58. fastmcp-2.10.6.dist-info/RECORD +0 -93
  59. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
  60. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
  61. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ from mcp.server.auth.provider import (
5
+ AccessToken,
6
+ )
7
+ from pydantic import AnyHttpUrl
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+ from starlette.responses import JSONResponse
10
+ from starlette.routing import BaseRoute, Route
11
+
12
+ from fastmcp.server.auth.auth import AuthProvider, TokenVerifier
13
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
14
+ from fastmcp.server.auth.registry import register_provider
15
+ from fastmcp.utilities.logging import get_logger
16
+ from fastmcp.utilities.types import NotSet, NotSetT
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class AuthKitProviderSettings(BaseSettings):
22
+ model_config = SettingsConfigDict(
23
+ env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
24
+ env_file=".env",
25
+ extra="ignore",
26
+ )
27
+
28
+ authkit_domain: AnyHttpUrl
29
+ base_url: AnyHttpUrl
30
+ required_scopes: list[str] | None = None
31
+
32
+
33
+ @register_provider("AUTHKIT")
34
+ class AuthKitProvider(AuthProvider):
35
+ """WorkOS AuthKit metadata provider for DCR (Dynamic Client Registration).
36
+
37
+ This provider implements WorkOS AuthKit integration using metadata forwarding
38
+ instead of OAuth proxying. This is the recommended approach for WorkOS DCR
39
+ as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
40
+ as a resource server.
41
+
42
+ IMPORTANT SETUP REQUIREMENTS:
43
+
44
+ 1. Enable Dynamic Client Registration in WorkOS Dashboard:
45
+ - Go to Applications → Configuration
46
+ - Toggle "Dynamic Client Registration" to enabled
47
+
48
+ 2. Configure your FastMCP server URL as a callback:
49
+ - Add your server URL to the Redirects tab in WorkOS dashboard
50
+ - Example: https://your-fastmcp-server.com/oauth2/callback
51
+
52
+ For detailed setup instructions, see:
53
+ https://workos.com/docs/authkit/mcp/integrating/token-verification
54
+
55
+ Example:
56
+ ```python
57
+ from fastmcp.server.auth.providers.workos import AuthKitProvider
58
+
59
+ # Create WorkOS metadata provider (JWT verifier created automatically)
60
+ workos_auth = AuthKitProvider(
61
+ authkit_domain="https://your-workos-domain.authkit.app",
62
+ base_url="https://your-fastmcp-server.com",
63
+ )
64
+
65
+ # Use with FastMCP
66
+ mcp = FastMCP("My App", auth=workos_auth)
67
+ ```
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
74
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
75
+ required_scopes: list[str] | None | NotSetT = NotSet,
76
+ token_verifier: TokenVerifier | None = None,
77
+ ):
78
+ """Initialize WorkOS metadata provider.
79
+
80
+ Args:
81
+ authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
82
+ base_url: Public URL of this FastMCP server
83
+ required_scopes: Optional list of scopes to require for all requests
84
+ token_verifier: Optional token verifier. If None, creates JWT verifier for WorkOS
85
+ """
86
+ super().__init__()
87
+
88
+ settings = AuthKitProviderSettings.model_validate(
89
+ {
90
+ k: v
91
+ for k, v in {
92
+ "authkit_domain": authkit_domain,
93
+ "base_url": base_url,
94
+ "required_scopes": required_scopes,
95
+ }.items()
96
+ if v is not NotSet
97
+ }
98
+ )
99
+
100
+ self.authkit_domain = str(settings.authkit_domain).rstrip("/")
101
+ self.base_url = str(settings.base_url).rstrip("/")
102
+
103
+ # Create default JWT verifier if none provided
104
+ if token_verifier is None:
105
+ token_verifier = JWTVerifier(
106
+ jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
107
+ issuer=self.authkit_domain,
108
+ algorithm="RS256",
109
+ required_scopes=settings.required_scopes,
110
+ )
111
+
112
+ self.token_verifier = token_verifier
113
+
114
+ async def verify_token(self, token: str) -> AccessToken | None:
115
+ """Verify a WorkOS token using the configured token verifier."""
116
+ return await self.token_verifier.verify_token(token)
117
+
118
+ def customize_auth_routes(self, routes: list[BaseRoute]) -> list[BaseRoute]:
119
+ """Add AuthKit metadata endpoints.
120
+
121
+ This adds:
122
+ - /.well-known/oauth-authorization-server (forwards AuthKit metadata)
123
+ - /.well-known/oauth-protected-resource (returns FastMCP resource info)
124
+ """
125
+
126
+ async def oauth_authorization_server_metadata(request):
127
+ """Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
128
+ try:
129
+ async with httpx.AsyncClient() as client:
130
+ response = await client.get(
131
+ f"{self.authkit_domain}/.well-known/oauth-authorization-server"
132
+ )
133
+ response.raise_for_status()
134
+ metadata = response.json()
135
+ return JSONResponse(metadata)
136
+ except Exception as e:
137
+ return JSONResponse(
138
+ {
139
+ "error": "server_error",
140
+ "error_description": f"Failed to fetch AuthKit metadata: {e}",
141
+ },
142
+ status_code=500,
143
+ )
144
+
145
+ async def oauth_protected_resource_metadata(request):
146
+ """Return FastMCP resource server metadata."""
147
+ return JSONResponse(
148
+ {
149
+ "resource": self.base_url,
150
+ "authorization_servers": [self.authkit_domain],
151
+ "bearer_methods_supported": ["header"],
152
+ }
153
+ )
154
+
155
+ routes.extend(
156
+ [
157
+ Route(
158
+ "/.well-known/oauth-authorization-server",
159
+ endpoint=oauth_authorization_server_metadata,
160
+ methods=["GET"],
161
+ ),
162
+ Route(
163
+ "/.well-known/oauth-protected-resource",
164
+ endpoint=oauth_protected_resource_metadata,
165
+ methods=["GET"],
166
+ ),
167
+ ]
168
+ )
169
+
170
+ return routes
@@ -0,0 +1,52 @@
1
+ """Provider registry for FastMCP auth providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import TYPE_CHECKING, TypeVar
7
+
8
+ if TYPE_CHECKING:
9
+ from fastmcp.server.auth.auth import AuthProvider
10
+
11
+ # Type variable for auth providers
12
+ T = TypeVar("T", bound="AuthProvider")
13
+
14
+
15
+ # Provider Registry
16
+ _PROVIDER_REGISTRY: dict[str, type[AuthProvider]] = {}
17
+
18
+
19
+ def register_provider(name: str) -> Callable[[type[T]], type[T]]:
20
+ """Decorator to register an auth provider with a given name.
21
+
22
+ Args:
23
+ name: The name to register the provider under (e.g., 'AUTHKIT')
24
+
25
+ Returns:
26
+ The decorated class
27
+
28
+ Example:
29
+ @register_provider('AUTHKIT')
30
+ class AuthKitProvider(AuthProvider):
31
+ ...
32
+ """
33
+
34
+ def decorator(cls: type[T]) -> type[T]:
35
+ _PROVIDER_REGISTRY[name.upper()] = cls
36
+ return cls
37
+
38
+ return decorator
39
+
40
+
41
+ def get_registered_provider(name: str) -> type[AuthProvider]:
42
+ """Get a registered provider by name.
43
+
44
+ Args:
45
+ name: The provider name (case-insensitive)
46
+
47
+ Returns:
48
+ The provider class if found, None otherwise
49
+ """
50
+ if name.upper() in _PROVIDER_REGISTRY:
51
+ return _PROVIDER_REGISTRY[name.upper()]
52
+ raise ValueError(f"Provider {name!r} has not been registered.")
fastmcp/server/context.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import copy
4
5
  import warnings
5
- from collections.abc import Generator
6
+ from collections.abc import Generator, Mapping
6
7
  from contextlib import contextmanager
7
8
  from contextvars import ContextVar, Token
8
9
  from dataclasses import dataclass
@@ -46,6 +47,18 @@ _current_context: ContextVar[Context | None] = ContextVar("context", default=Non
46
47
  _flush_lock = asyncio.Lock()
47
48
 
48
49
 
50
+ @dataclass
51
+ class LogData:
52
+ """Data object for passing log arguments to client-side handlers.
53
+
54
+ This provides an interface to match the Python standard library logging,
55
+ for compatibility with structured logging.
56
+ """
57
+
58
+ msg: str
59
+ extra: Mapping[str, Any] | None = None
60
+
61
+
49
62
  @contextmanager
50
63
  def set_context(context: Context) -> Generator[Context, None, None]:
51
64
  token = _current_context.set(context)
@@ -83,9 +96,19 @@ class Context:
83
96
  request_id = ctx.request_id
84
97
  client_id = ctx.client_id
85
98
 
99
+ # Manage state across the request
100
+ ctx.set_state("key", "value")
101
+ value = ctx.get_state("key")
102
+
86
103
  return str(x)
87
104
  ```
88
105
 
106
+ State Management:
107
+ Context objects maintain a state dictionary that can be used to store and share
108
+ data across middleware and tool calls within a request. When a new context
109
+ is created (nested contexts), it inherits a copy of its parent's state, ensuring
110
+ that modifications in child contexts don't affect parent contexts.
111
+
89
112
  The context parameter name can be anything as long as it's annotated with Context.
90
113
  The context is optional - tools that don't need it can omit the parameter.
91
114
 
@@ -95,9 +118,15 @@ class Context:
95
118
  self.fastmcp = fastmcp
96
119
  self._tokens: list[Token] = []
97
120
  self._notification_queue: set[str] = set() # Dedupe notifications
121
+ self._state: dict[str, Any] = {}
98
122
 
99
123
  async def __aenter__(self) -> Context:
100
124
  """Enter the context manager and set this context as the current context."""
125
+ parent_context = _current_context.get(None)
126
+ if parent_context is not None:
127
+ # Inherit state from parent context
128
+ self._state = copy.deepcopy(parent_context._state)
129
+
101
130
  # Always set this context and save the token
102
131
  token = _current_context.set(self)
103
132
  self._tokens.append(token)
@@ -113,7 +142,7 @@ class Context:
113
142
  _current_context.reset(token)
114
143
 
115
144
  @property
116
- def request_context(self) -> RequestContext:
145
+ def request_context(self) -> RequestContext[ServerSession, Any, Request]:
117
146
  """Access to the underlying request context.
118
147
 
119
148
  If called outside of a request context, this will raise a ValueError.
@@ -167,6 +196,7 @@ class Context:
167
196
  message: str,
168
197
  level: LoggingLevel | None = None,
169
198
  logger_name: str | None = None,
199
+ extra: Mapping[str, Any] | None = None,
170
200
  ) -> None:
171
201
  """Send a log message to the client.
172
202
 
@@ -175,12 +205,14 @@ class Context:
175
205
  level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
176
206
  "alert", or "emergency". Default is "info".
177
207
  logger_name: Optional logger name
208
+ extra: Optional mapping for additional arguments
178
209
  """
179
210
  if level is None:
180
211
  level = "info"
212
+ data = LogData(msg=message, extra=extra)
181
213
  await self.session.send_log_message(
182
214
  level=level,
183
- data=message,
215
+ data=data,
184
216
  logger=logger_name,
185
217
  related_request_id=self.request_id,
186
218
  )
@@ -200,35 +232,48 @@ class Context:
200
232
  return str(self.request_context.request_id)
201
233
 
202
234
  @property
203
- def session_id(self) -> str | None:
204
- """Get the MCP session ID for HTTP transports.
235
+ def session_id(self) -> str:
236
+ """Get the MCP session ID for ALL transports.
205
237
 
206
238
  Returns the session ID that can be used as a key for session-based
207
239
  data storage (e.g., Redis) to share data between tool calls within
208
240
  the same client session.
209
241
 
210
242
  Returns:
211
- The session ID for HTTP transports (SSE, StreamableHTTP), or None
212
- for stdio and in-memory transports which don't use session IDs.
243
+ The session ID for StreamableHTTP transports, or a generated ID
244
+ for other transports.
213
245
 
214
246
  Example:
215
247
  ```python
216
248
  @server.tool
217
249
  def store_data(data: dict, ctx: Context) -> str:
218
- if session_id := ctx.session_id:
219
- redis_client.set(f"session:{session_id}:data", json.dumps(data))
220
- return f"Data stored for session {session_id}"
221
- return "No session ID available (stdio/memory transport)"
250
+ session_id = ctx.session_id
251
+ redis_client.set(f"session:{session_id}:data", json.dumps(data))
252
+ return f"Data stored for session {session_id}"
222
253
  ```
223
254
  """
224
- try:
225
- from fastmcp.server.dependencies import get_http_headers
255
+ request_ctx = self.request_context
256
+ session = request_ctx.session
226
257
 
227
- headers = get_http_headers(include_all=True)
228
- return headers.get("mcp-session-id")
229
- except RuntimeError:
230
- # No HTTP context available (stdio/in-memory transport)
231
- return None
258
+ # Try to get the session ID from the session attributes
259
+ session_id = getattr(session, "_fastmcp_id", None)
260
+ if session_id is not None:
261
+ return session_id
262
+
263
+ # Try to get the session ID from the http request headers
264
+ request = request_ctx.request
265
+ if request:
266
+ session_id = request.headers.get("mcp-session-id")
267
+
268
+ # Generate a session ID if it doesn't exist.
269
+ if session_id is None:
270
+ from uuid import uuid4
271
+
272
+ session_id = str(uuid4())
273
+
274
+ # Save the session id to the session attributes
275
+ setattr(session, "_fastmcp_id", session_id)
276
+ return session_id
232
277
 
233
278
  @property
234
279
  def session(self) -> ServerSession:
@@ -236,21 +281,49 @@ class Context:
236
281
  return self.request_context.session
237
282
 
238
283
  # Convenience methods for common log levels
239
- async def debug(self, message: str, logger_name: str | None = None) -> None:
284
+ async def debug(
285
+ self,
286
+ message: str,
287
+ logger_name: str | None = None,
288
+ extra: Mapping[str, Any] | None = None,
289
+ ) -> None:
240
290
  """Send a debug log message."""
241
- await self.log(level="debug", message=message, logger_name=logger_name)
291
+ await self.log(
292
+ level="debug", message=message, logger_name=logger_name, extra=extra
293
+ )
242
294
 
243
- async def info(self, message: str, logger_name: str | None = None) -> None:
295
+ async def info(
296
+ self,
297
+ message: str,
298
+ logger_name: str | None = None,
299
+ extra: Mapping[str, Any] | None = None,
300
+ ) -> None:
244
301
  """Send an info log message."""
245
- await self.log(level="info", message=message, logger_name=logger_name)
302
+ await self.log(
303
+ level="info", message=message, logger_name=logger_name, extra=extra
304
+ )
246
305
 
247
- async def warning(self, message: str, logger_name: str | None = None) -> None:
306
+ async def warning(
307
+ self,
308
+ message: str,
309
+ logger_name: str | None = None,
310
+ extra: Mapping[str, Any] | None = None,
311
+ ) -> None:
248
312
  """Send a warning log message."""
249
- await self.log(level="warning", message=message, logger_name=logger_name)
313
+ await self.log(
314
+ level="warning", message=message, logger_name=logger_name, extra=extra
315
+ )
250
316
 
251
- async def error(self, message: str, logger_name: str | None = None) -> None:
317
+ async def error(
318
+ self,
319
+ message: str,
320
+ logger_name: str | None = None,
321
+ extra: Mapping[str, Any] | None = None,
322
+ ) -> None:
252
323
  """Send an error log message."""
253
- await self.log(level="error", message=message, logger_name=logger_name)
324
+ await self.log(
325
+ level="error", message=message, logger_name=logger_name, extra=extra
326
+ )
254
327
 
255
328
  async def list_roots(self) -> list[Root]:
256
329
  """List the roots available to the server, as indicated by the client."""
@@ -455,6 +528,14 @@ class Context:
455
528
 
456
529
  return fastmcp.server.dependencies.get_http_request()
457
530
 
531
+ def set_state(self, key: str, value: Any) -> None:
532
+ """Set a value in the context state."""
533
+ self._state[key] = value
534
+
535
+ def get_state(self, key: str) -> Any:
536
+ """Get a value from the context state. Returns None if the key is not found."""
537
+ return self._state.get(key)
538
+
458
539
  def _queue_tool_list_changed(self) -> None:
459
540
  """Queue a tool list changed notification."""
460
541
  self._notification_queue.add("notifications/tools/list_changed")
@@ -37,9 +37,14 @@ def get_context() -> Context:
37
37
 
38
38
 
39
39
  def get_http_request() -> Request:
40
- from fastmcp.server.http import _current_http_request
40
+ from mcp.server.lowlevel.server import request_ctx
41
+
42
+ request = None
43
+ try:
44
+ request = request_ctx.get().request
45
+ except LookupError:
46
+ pass
41
47
 
42
- request = _current_http_request.get()
43
48
  if request is None:
44
49
  raise RuntimeError("No active HTTP request found.")
45
50
  return request
@@ -72,6 +77,8 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
72
77
  "proxy-authenticate",
73
78
  "proxy-authorization",
74
79
  "proxy-connection",
80
+ # MCP-related headers
81
+ "mcp-session-id",
75
82
  }
76
83
  # (just in case)
77
84
  if not all(h.lower() == h for h in exclude_headers):
fastmcp/server/http.py CHANGED
@@ -3,18 +3,20 @@ from __future__ import annotations
3
3
  from collections.abc import AsyncGenerator, Callable, Generator
4
4
  from contextlib import asynccontextmanager, contextmanager
5
5
  from contextvars import ContextVar
6
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, cast
7
7
 
8
8
  from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
9
9
  from mcp.server.auth.middleware.bearer_auth import (
10
10
  BearerAuthBackend,
11
11
  RequireAuthMiddleware,
12
12
  )
13
+ from mcp.server.auth.provider import TokenVerifier as TokenVerifierProtocol
13
14
  from mcp.server.auth.routes import create_auth_routes
14
15
  from mcp.server.lowlevel.server import LifespanResultT
15
16
  from mcp.server.sse import SseServerTransport
16
17
  from mcp.server.streamable_http import EventStore
17
18
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
19
+ from pydantic import AnyHttpUrl
18
20
  from starlette.applications import Starlette
19
21
  from starlette.middleware import Middleware
20
22
  from starlette.middleware.authentication import AuthenticationMiddleware
@@ -23,7 +25,7 @@ from starlette.responses import Response
23
25
  from starlette.routing import BaseRoute, Mount, Route
24
26
  from starlette.types import Lifespan, Receive, Scope, Send
25
27
 
26
- from fastmcp.server.auth.auth import OAuthProvider
28
+ from fastmcp.server.auth.auth import AuthProvider, OAuthProvider, TokenVerifier
27
29
  from fastmcp.utilities.logging import get_logger
28
30
 
29
31
  if TYPE_CHECKING:
@@ -70,39 +72,46 @@ class RequestContextMiddleware:
70
72
 
71
73
 
72
74
  def setup_auth_middleware_and_routes(
73
- auth: OAuthProvider,
74
- ) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
75
+ auth: AuthProvider,
76
+ ) -> tuple[list[Middleware], list[Route], list[str]]:
75
77
  """Set up authentication middleware and routes if auth is enabled.
76
78
 
77
79
  Args:
78
- auth: The OAuthProvider authorization server provider
80
+ auth: An AuthProvider for authentication (TokenVerifier or OAuthProvider)
79
81
 
80
82
  Returns:
81
83
  Tuple of (middleware, auth_routes, required_scopes)
82
84
  """
83
- middleware: list[Middleware] = []
84
- auth_routes: list[BaseRoute] = []
85
- required_scopes: list[str] = []
86
-
87
- middleware = [
85
+ middleware: list[Middleware] = [
88
86
  Middleware(
89
87
  AuthenticationMiddleware,
90
- backend=BearerAuthBackend(auth),
88
+ backend=BearerAuthBackend(cast(TokenVerifierProtocol, auth)),
91
89
  ),
92
90
  Middleware(AuthContextMiddleware),
93
91
  ]
94
92
 
95
- required_scopes = auth.required_scopes or []
96
-
97
- auth_routes.extend(
98
- create_auth_routes(
99
- provider=auth,
100
- issuer_url=auth.issuer_url,
101
- service_documentation_url=auth.service_documentation_url,
102
- client_registration_options=auth.client_registration_options,
103
- revocation_options=auth.revocation_options,
93
+ auth_routes: list[Route] = []
94
+ required_scopes: list[str] = auth.required_scopes or []
95
+
96
+ # Check if it's an OAuthProvider (has OAuth server capability)
97
+ if isinstance(auth, OAuthProvider):
98
+ # OAuthProvider: create standard OAuth routes first
99
+ standard_routes = list(
100
+ create_auth_routes(
101
+ provider=auth,
102
+ issuer_url=auth.issuer_url,
103
+ service_documentation_url=auth.service_documentation_url,
104
+ client_registration_options=auth.client_registration_options,
105
+ revocation_options=auth.revocation_options,
106
+ )
104
107
  )
105
- )
108
+
109
+ # Allow provider to customize routes (e.g., for proxy behavior or metadata endpoints)
110
+ auth_routes = auth.customize_auth_routes(standard_routes)
111
+ else:
112
+ # Simple AuthProvider or TokenVerifier: start with empty routes
113
+ # Allow provider to add custom routes (e.g., metadata endpoints)
114
+ auth_routes = auth.customize_auth_routes([])
106
115
 
107
116
  return middleware, auth_routes, required_scopes
108
117
 
@@ -139,7 +148,7 @@ def create_sse_app(
139
148
  server: FastMCP[LifespanResultT],
140
149
  message_path: str,
141
150
  sse_path: str,
142
- auth: OAuthProvider | None = None,
151
+ auth: AuthProvider | None = None,
143
152
  debug: bool = False,
144
153
  routes: list[BaseRoute] | None = None,
145
154
  middleware: list[Middleware] | None = None,
@@ -150,7 +159,7 @@ def create_sse_app(
150
159
  server: The FastMCP server instance
151
160
  message_path: Path for SSE messages
152
161
  sse_path: Path for SSE connections
153
- auth: Optional auth provider
162
+ auth: Optional authentication provider (AuthProvider)
154
163
  debug: Whether to enable debug mode
155
164
  routes: Optional list of custom routes
156
165
  middleware: Optional list of middleware
@@ -175,8 +184,6 @@ def create_sse_app(
175
184
  return Response()
176
185
 
177
186
  # Get auth middleware and routes
178
-
179
- # Add SSE routes with or without auth
180
187
  if auth:
181
188
  auth_middleware, auth_routes, required_scopes = (
182
189
  setup_auth_middleware_and_routes(auth)
@@ -184,18 +191,32 @@ def create_sse_app(
184
191
 
185
192
  server_routes.extend(auth_routes)
186
193
  server_middleware.extend(auth_middleware)
194
+
195
+ # Determine resource_metadata_url for TokenVerifier
196
+ resource_metadata_url = None
197
+ if isinstance(auth, TokenVerifier) and auth.resource_server_url:
198
+ # Add .well-known path for RFC 9728 compliance
199
+ resource_metadata_url = AnyHttpUrl(
200
+ str(auth.resource_server_url).rstrip("/")
201
+ + "/.well-known/oauth-protected-resource"
202
+ )
203
+
187
204
  # Auth is enabled, wrap endpoints with RequireAuthMiddleware
188
205
  server_routes.append(
189
206
  Route(
190
207
  sse_path,
191
- endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
208
+ endpoint=RequireAuthMiddleware(
209
+ handle_sse, required_scopes, resource_metadata_url
210
+ ),
192
211
  methods=["GET"],
193
212
  )
194
213
  )
195
214
  server_routes.append(
196
215
  Mount(
197
216
  message_path,
198
- app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
217
+ app=RequireAuthMiddleware(
218
+ sse.handle_post_message, required_scopes, resource_metadata_url
219
+ ),
199
220
  )
200
221
  )
201
222
  else:
@@ -243,7 +264,7 @@ def create_streamable_http_app(
243
264
  server: FastMCP[LifespanResultT],
244
265
  streamable_http_path: str,
245
266
  event_store: EventStore | None = None,
246
- auth: OAuthProvider | None = None,
267
+ auth: AuthProvider | None = None,
247
268
  json_response: bool = False,
248
269
  stateless_http: bool = False,
249
270
  debug: bool = False,
@@ -256,7 +277,7 @@ def create_streamable_http_app(
256
277
  server: The FastMCP server instance
257
278
  streamable_http_path: Path for StreamableHTTP connections
258
279
  event_store: Optional event store for session management
259
- auth: Optional auth provider
280
+ auth: Optional authentication provider (AuthProvider)
260
281
  json_response: Whether to use JSON response format
261
282
  stateless_http: Whether to use stateless mode (new transport per request)
262
283
  debug: Whether to enable debug mode
@@ -314,11 +335,22 @@ def create_streamable_http_app(
314
335
  server_routes.extend(auth_routes)
315
336
  server_middleware.extend(auth_middleware)
316
337
 
338
+ # Determine resource_metadata_url for TokenVerifier
339
+ resource_metadata_url = None
340
+ if isinstance(auth, TokenVerifier) and auth.resource_server_url:
341
+ # Add .well-known path for RFC 9728 compliance
342
+ resource_metadata_url = AnyHttpUrl(
343
+ str(auth.resource_server_url).rstrip("/")
344
+ + "/.well-known/oauth-protected-resource"
345
+ )
346
+
317
347
  # Auth is enabled, wrap endpoint with RequireAuthMiddleware
318
348
  server_routes.append(
319
349
  Mount(
320
350
  streamable_http_path,
321
- app=RequireAuthMiddleware(handle_streamable_http, required_scopes),
351
+ app=RequireAuthMiddleware(
352
+ handle_streamable_http, required_scopes, resource_metadata_url
353
+ ),
322
354
  )
323
355
  )
324
356
  else: