fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc1__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 +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  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 +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  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 +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  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 +2 -2
  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.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.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.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,172 @@
1
+ """Supabase authentication provider for FastMCP.
2
+
3
+ This module provides SupabaseProvider - a complete authentication solution that integrates
4
+ with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
5
+ for seamless MCP client authentication.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import httpx
11
+ from pydantic import AnyHttpUrl, field_validator
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict
13
+ from starlette.responses import JSONResponse
14
+ from starlette.routing import Route
15
+
16
+ from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
17
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
18
+ from fastmcp.settings import ENV_FILE
19
+ from fastmcp.utilities.auth import parse_scopes
20
+ from fastmcp.utilities.logging import get_logger
21
+ from fastmcp.utilities.types import NotSet, NotSetT
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class SupabaseProviderSettings(BaseSettings):
27
+ model_config = SettingsConfigDict(
28
+ env_prefix="FASTMCP_SERVER_AUTH_SUPABASE_",
29
+ env_file=ENV_FILE,
30
+ extra="ignore",
31
+ )
32
+
33
+ project_url: AnyHttpUrl
34
+ base_url: AnyHttpUrl
35
+ required_scopes: list[str] | None = None
36
+
37
+ @field_validator("required_scopes", mode="before")
38
+ @classmethod
39
+ def _parse_scopes(cls, v):
40
+ return parse_scopes(v)
41
+
42
+
43
+ class SupabaseProvider(RemoteAuthProvider):
44
+ """Supabase metadata provider for DCR (Dynamic Client Registration).
45
+
46
+ This provider implements Supabase Auth integration using metadata forwarding.
47
+ This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
48
+ as a resource server, verifying JWTs issued by Supabase Auth.
49
+
50
+ IMPORTANT SETUP REQUIREMENTS:
51
+
52
+ 1. Supabase Project Setup:
53
+ - Create a Supabase project at https://supabase.com
54
+ - Note your project URL (e.g., "https://abc123.supabase.co")
55
+ - For projects created after May 1st, 2025, asymmetric RS256 keys are used by default
56
+ - For older projects, consider migrating to asymmetric keys for better security
57
+
58
+ 2. JWT Verification:
59
+ - FastMCP verifies JWTs using the JWKS endpoint at {project_url}/auth/v1/.well-known/jwks.json
60
+ - JWTs are issued by {project_url}/auth/v1
61
+ - Tokens are cached for up to 10 minutes by Supabase's edge servers
62
+
63
+ For detailed setup instructions, see:
64
+ https://supabase.com/docs/guides/auth/jwts
65
+
66
+ Example:
67
+ ```python
68
+ from fastmcp.server.auth.providers.supabase import SupabaseProvider
69
+
70
+ # Create Supabase metadata provider (JWT verifier created automatically)
71
+ supabase_auth = SupabaseProvider(
72
+ project_url="https://abc123.supabase.co",
73
+ base_url="https://your-fastmcp-server.com",
74
+ )
75
+
76
+ # Use with FastMCP
77
+ mcp = FastMCP("My App", auth=supabase_auth)
78
+ ```
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ project_url: AnyHttpUrl | str | NotSetT = NotSet,
85
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
86
+ required_scopes: list[str] | None | NotSetT = NotSet,
87
+ token_verifier: TokenVerifier | None = None,
88
+ ):
89
+ """Initialize Supabase metadata provider.
90
+
91
+ Args:
92
+ project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
93
+ base_url: Public URL of this FastMCP server
94
+ required_scopes: Optional list of scopes to require for all requests
95
+ token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
96
+ """
97
+ settings = SupabaseProviderSettings.model_validate(
98
+ {
99
+ k: v
100
+ for k, v in {
101
+ "project_url": project_url,
102
+ "base_url": base_url,
103
+ "required_scopes": required_scopes,
104
+ }.items()
105
+ if v is not NotSet
106
+ }
107
+ )
108
+
109
+ self.project_url = str(settings.project_url).rstrip("/")
110
+ self.base_url = str(settings.base_url).rstrip("/")
111
+
112
+ # Create default JWT verifier if none provided
113
+ if token_verifier is None:
114
+ token_verifier = JWTVerifier(
115
+ jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
116
+ issuer=f"{self.project_url}/auth/v1",
117
+ algorithm="ES256", # Supabase uses ES256 for asymmetric keys
118
+ required_scopes=settings.required_scopes,
119
+ )
120
+
121
+ # Initialize RemoteAuthProvider with Supabase as the authorization server
122
+ super().__init__(
123
+ token_verifier=token_verifier,
124
+ authorization_servers=[AnyHttpUrl(f"{self.project_url}/auth/v1")],
125
+ base_url=self.base_url,
126
+ )
127
+
128
+ def get_routes(
129
+ self,
130
+ mcp_path: str | None = None,
131
+ ) -> list[Route]:
132
+ """Get OAuth routes including Supabase authorization server metadata forwarding.
133
+
134
+ This returns the standard protected resource routes plus an authorization server
135
+ metadata endpoint that forwards Supabase's OAuth metadata to clients.
136
+
137
+ Args:
138
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
139
+ This is used to advertise the resource URL in metadata.
140
+ """
141
+ # Get the standard protected resource routes from RemoteAuthProvider
142
+ routes = super().get_routes(mcp_path)
143
+
144
+ async def oauth_authorization_server_metadata(request):
145
+ """Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
146
+ try:
147
+ async with httpx.AsyncClient() as client:
148
+ response = await client.get(
149
+ f"{self.project_url}/auth/v1/.well-known/oauth-authorization-server"
150
+ )
151
+ response.raise_for_status()
152
+ metadata = response.json()
153
+ return JSONResponse(metadata)
154
+ except Exception as e:
155
+ return JSONResponse(
156
+ {
157
+ "error": "server_error",
158
+ "error_description": f"Failed to fetch Supabase metadata: {e}",
159
+ },
160
+ status_code=500,
161
+ )
162
+
163
+ # Add Supabase authorization server metadata forwarding
164
+ routes.append(
165
+ Route(
166
+ "/.well-known/oauth-authorization-server",
167
+ endpoint=oauth_authorization_server_metadata,
168
+ methods=["GET"],
169
+ )
170
+ )
171
+
172
+ return routes
@@ -10,9 +10,8 @@ Choose based on your WorkOS setup and authentication requirements.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- from typing import Any
14
-
15
13
  import httpx
14
+ from key_value.aio.protocols import AsyncKeyValue
16
15
  from pydantic import AnyHttpUrl, SecretStr, field_validator
17
16
  from pydantic_settings import BaseSettings, SettingsConfigDict
18
17
  from starlette.responses import JSONResponse
@@ -21,9 +20,9 @@ from starlette.routing import Route
21
20
  from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
22
21
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
23
22
  from fastmcp.server.auth.providers.jwt import JWTVerifier
23
+ from fastmcp.settings import ENV_FILE
24
24
  from fastmcp.utilities.auth import parse_scopes
25
25
  from fastmcp.utilities.logging import get_logger
26
- from fastmcp.utilities.storage import KVStorage
27
26
  from fastmcp.utilities.types import NotSet, NotSetT
28
27
 
29
28
  logger = get_logger(__name__)
@@ -34,7 +33,7 @@ class WorkOSProviderSettings(BaseSettings):
34
33
 
35
34
  model_config = SettingsConfigDict(
36
35
  env_prefix="FASTMCP_SERVER_AUTH_WORKOS_",
37
- env_file=".env",
36
+ env_file=ENV_FILE,
38
37
  extra="ignore",
39
38
  )
40
39
 
@@ -42,6 +41,7 @@ class WorkOSProviderSettings(BaseSettings):
42
41
  client_secret: SecretStr | None = None
43
42
  authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
44
43
  base_url: AnyHttpUrl | str | None = None
44
+ issuer_url: AnyHttpUrl | str | None = None
45
45
  redirect_path: str | None = None
46
46
  required_scopes: list[str] | None = None
47
47
  timeout_seconds: int | None = None
@@ -166,11 +166,12 @@ class WorkOSProvider(OAuthProxy):
166
166
  client_secret: str | NotSetT = NotSet,
167
167
  authkit_domain: str | NotSetT = NotSet,
168
168
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
169
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
169
170
  redirect_path: str | NotSetT = NotSet,
170
171
  required_scopes: list[str] | None | NotSetT = NotSet,
171
172
  timeout_seconds: int | NotSetT = NotSet,
172
173
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
173
- client_storage: KVStorage | None = None,
174
+ client_storage: AsyncKeyValue | None = None,
174
175
  ):
175
176
  """Initialize WorkOS OAuth provider.
176
177
 
@@ -178,14 +179,15 @@ class WorkOSProvider(OAuthProxy):
178
179
  client_id: WorkOS client ID
179
180
  client_secret: WorkOS client secret
180
181
  authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
181
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
182
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
183
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
184
+ to avoid 404s during discovery when mounting under a path.
182
185
  redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
183
186
  required_scopes: Required OAuth scopes (no default)
184
187
  timeout_seconds: HTTP request timeout for WorkOS API calls
185
188
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
186
189
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
187
- client_storage: Storage implementation for OAuth client registrations.
188
- Defaults to file-based storage if not specified.
190
+ client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
189
191
  """
190
192
 
191
193
  settings = WorkOSProviderSettings.model_validate(
@@ -196,6 +198,7 @@ class WorkOSProvider(OAuthProxy):
196
198
  "client_secret": client_secret,
197
199
  "authkit_domain": authkit_domain,
198
200
  "base_url": base_url,
201
+ "issuer_url": issuer_url,
199
202
  "redirect_path": redirect_path,
200
203
  "required_scopes": required_scopes,
201
204
  "timeout_seconds": timeout_seconds,
@@ -249,7 +252,8 @@ class WorkOSProvider(OAuthProxy):
249
252
  token_verifier=token_verifier,
250
253
  base_url=settings.base_url,
251
254
  redirect_path=settings.redirect_path,
252
- issuer_url=settings.base_url,
255
+ issuer_url=settings.issuer_url
256
+ or settings.base_url, # Default to base_url if not specified
253
257
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
254
258
  client_storage=client_storage,
255
259
  )
@@ -264,7 +268,7 @@ class WorkOSProvider(OAuthProxy):
264
268
  class AuthKitProviderSettings(BaseSettings):
265
269
  model_config = SettingsConfigDict(
266
270
  env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
267
- env_file=".env",
271
+ env_file=ENV_FILE,
268
272
  extra="ignore",
269
273
  )
270
274
 
@@ -364,7 +368,6 @@ class AuthKitProvider(RemoteAuthProvider):
364
368
  def get_routes(
365
369
  self,
366
370
  mcp_path: str | None = None,
367
- mcp_endpoint: Any | None = None,
368
371
  ) -> list[Route]:
369
372
  """Get OAuth routes including AuthKit authorization server metadata forwarding.
370
373
 
@@ -373,10 +376,10 @@ class AuthKitProvider(RemoteAuthProvider):
373
376
 
374
377
  Args:
375
378
  mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
376
- mcp_endpoint: The MCP endpoint handler to protect with auth
379
+ This is used to advertise the resource URL in metadata.
377
380
  """
378
381
  # Get the standard protected resource routes from RemoteAuthProvider
379
- routes = super().get_routes(mcp_path, mcp_endpoint)
382
+ routes = super().get_routes(mcp_path)
380
383
 
381
384
  async def oauth_authorization_server_metadata(request):
382
385
  """Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
fastmcp/server/context.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import copy
5
4
  import inspect
5
+ import logging
6
6
  import warnings
7
7
  import weakref
8
8
  from collections.abc import Generator, Mapping, Sequence
@@ -10,8 +10,10 @@ from contextlib import contextmanager
10
10
  from contextvars import ContextVar, Token
11
11
  from dataclasses import dataclass
12
12
  from enum import Enum
13
+ from logging import Logger
13
14
  from typing import Any, Literal, cast, get_origin, overload
14
15
 
16
+ import anyio
15
17
  from mcp import LoggingLevel, ServerSession
16
18
  from mcp.server.lowlevel.helper_types import ReadResourceContents
17
19
  from mcp.server.lowlevel.server import request_ctx
@@ -44,14 +46,21 @@ from fastmcp.server.elicitation import (
44
46
  get_elicitation_schema,
45
47
  )
46
48
  from fastmcp.server.server import FastMCP
47
- from fastmcp.utilities.logging import get_logger
49
+ from fastmcp.utilities.logging import _clamp_logger, get_logger
48
50
  from fastmcp.utilities.types import get_cached_typeadapter
49
51
 
50
- logger = get_logger(__name__)
52
+ logger: Logger = get_logger(name=__name__)
53
+ to_client_logger: Logger = logger.getChild(suffix="to_client")
54
+
55
+ # Convert all levels of server -> client messages to debug level
56
+ # This clamp can be undone at runtime by calling `_unclamp_logger` or calling
57
+ # `_clamp_logger` with a different max level.
58
+ _clamp_logger(logger=to_client_logger, max_level="DEBUG")
59
+
51
60
 
52
61
  T = TypeVar("T", default=Any)
53
62
  _current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
54
- _flush_lock = asyncio.Lock()
63
+ _flush_lock = anyio.Lock()
55
64
 
56
65
 
57
66
  @dataclass
@@ -66,6 +75,18 @@ class LogData:
66
75
  extra: Mapping[str, Any] | None = None
67
76
 
68
77
 
78
+ _mcp_level_to_python_level = {
79
+ "debug": logging.DEBUG,
80
+ "info": logging.INFO,
81
+ "notice": logging.INFO,
82
+ "warning": logging.WARNING,
83
+ "error": logging.ERROR,
84
+ "critical": logging.CRITICAL,
85
+ "alert": logging.CRITICAL,
86
+ "emergency": logging.CRITICAL,
87
+ }
88
+
89
+
69
90
  @contextmanager
70
91
  def set_context(context: Context) -> Generator[Context, None, None]:
71
92
  token = _current_context.set(context)
@@ -205,7 +226,7 @@ class Context:
205
226
  """
206
227
  if self.fastmcp is None:
207
228
  raise ValueError("Context is not available outside of a request")
208
- return await self.fastmcp._mcp_read_resource(uri)
229
+ return await self.fastmcp._read_resource_mcp(uri)
209
230
 
210
231
  async def log(
211
232
  self,
@@ -216,6 +237,8 @@ class Context:
216
237
  ) -> None:
217
238
  """Send a log message to the client.
218
239
 
240
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
241
+
219
242
  Args:
220
243
  message: Log message
221
244
  level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
@@ -223,13 +246,13 @@ class Context:
223
246
  logger_name: Optional logger name
224
247
  extra: Optional mapping for additional arguments
225
248
  """
226
- if level is None:
227
- level = "info"
228
249
  data = LogData(msg=message, extra=extra)
229
- await self.session.send_log_message(
230
- level=level,
250
+
251
+ await _log_to_server_and_client(
231
252
  data=data,
232
- logger=logger_name,
253
+ session=self.session,
254
+ level=level or "info",
255
+ logger_name=logger_name,
233
256
  related_request_id=self.request_id,
234
257
  )
235
258
 
@@ -303,9 +326,14 @@ class Context:
303
326
  logger_name: str | None = None,
304
327
  extra: Mapping[str, Any] | None = None,
305
328
  ) -> None:
306
- """Send a debug log message."""
329
+ """Send a `DEBUG`-level message to the connected MCP Client.
330
+
331
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
307
332
  await self.log(
308
- level="debug", message=message, logger_name=logger_name, extra=extra
333
+ level="debug",
334
+ message=message,
335
+ logger_name=logger_name,
336
+ extra=extra,
309
337
  )
310
338
 
311
339
  async def info(
@@ -314,9 +342,14 @@ class Context:
314
342
  logger_name: str | None = None,
315
343
  extra: Mapping[str, Any] | None = None,
316
344
  ) -> None:
317
- """Send an info log message."""
345
+ """Send a `INFO`-level message to the connected MCP Client.
346
+
347
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
318
348
  await self.log(
319
- level="info", message=message, logger_name=logger_name, extra=extra
349
+ level="info",
350
+ message=message,
351
+ logger_name=logger_name,
352
+ extra=extra,
320
353
  )
321
354
 
322
355
  async def warning(
@@ -325,9 +358,14 @@ class Context:
325
358
  logger_name: str | None = None,
326
359
  extra: Mapping[str, Any] | None = None,
327
360
  ) -> None:
328
- """Send a warning log message."""
361
+ """Send a `WARNING`-level message to the connected MCP Client.
362
+
363
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
329
364
  await self.log(
330
- level="warning", message=message, logger_name=logger_name, extra=extra
365
+ level="warning",
366
+ message=message,
367
+ logger_name=logger_name,
368
+ extra=extra,
331
369
  )
332
370
 
333
371
  async def error(
@@ -336,9 +374,14 @@ class Context:
336
374
  logger_name: str | None = None,
337
375
  extra: Mapping[str, Any] | None = None,
338
376
  ) -> None:
339
- """Send an error log message."""
377
+ """Send a `ERROR`-level message to the connected MCP Client.
378
+
379
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
340
380
  await self.log(
341
- level="error", message=message, logger_name=logger_name, extra=extra
381
+ level="error",
382
+ message=message,
383
+ logger_name=logger_name,
384
+ extra=extra,
342
385
  )
343
386
 
344
387
  async def list_roots(self) -> list[Root]:
@@ -592,30 +635,14 @@ class Context:
592
635
  def _queue_tool_list_changed(self) -> None:
593
636
  """Queue a tool list changed notification."""
594
637
  self._notification_queue.add("notifications/tools/list_changed")
595
- self._try_flush_notifications()
596
638
 
597
639
  def _queue_resource_list_changed(self) -> None:
598
640
  """Queue a resource list changed notification."""
599
641
  self._notification_queue.add("notifications/resources/list_changed")
600
- self._try_flush_notifications()
601
642
 
602
643
  def _queue_prompt_list_changed(self) -> None:
603
644
  """Queue a prompt list changed notification."""
604
645
  self._notification_queue.add("notifications/prompts/list_changed")
605
- self._try_flush_notifications()
606
-
607
- def _try_flush_notifications(self) -> None:
608
- """Synchronous method that attempts to flush notifications if we're in an async context."""
609
- try:
610
- # Check if we're in an async context
611
- loop = asyncio.get_running_loop()
612
- if loop and not loop.is_running():
613
- return
614
- # Schedule flush as a task (fire-and-forget)
615
- asyncio.create_task(self._flush_notifications())
616
- except RuntimeError:
617
- # No event loop - will flush later
618
- pass
619
646
 
620
647
  async def _flush_notifications(self) -> None:
621
648
  """Send all queued notifications."""
@@ -675,3 +702,31 @@ def _parse_model_preferences(
675
702
  raise ValueError(
676
703
  "model_preferences must be one of: ModelPreferences, str, list[str], or None."
677
704
  )
705
+
706
+
707
+ async def _log_to_server_and_client(
708
+ data: LogData,
709
+ session: ServerSession,
710
+ level: LoggingLevel,
711
+ logger_name: str | None = None,
712
+ related_request_id: str | None = None,
713
+ ) -> None:
714
+ """Log a message to the server and client."""
715
+
716
+ msg_prefix = f"Sending {level.upper()} to client"
717
+
718
+ if logger_name:
719
+ msg_prefix += f" ({logger_name})"
720
+
721
+ to_client_logger.log(
722
+ level=_mcp_level_to_python_level[level],
723
+ msg=f"{msg_prefix}: {data.msg}",
724
+ extra=data.extra,
725
+ )
726
+
727
+ await session.send_log_message(
728
+ level=level,
729
+ data=data,
730
+ logger=logger_name,
731
+ related_request_id=related_request_id,
732
+ )
fastmcp/server/http.py CHANGED
@@ -6,6 +6,7 @@ from contextvars import ContextVar
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
9
+ from mcp.server.auth.routes import build_resource_metadata_url
9
10
  from mcp.server.lowlevel.server import LifespanResultT
10
11
  from mcp.server.sse import SseServerTransport
11
12
  from mcp.server.streamable_http import EventStore
@@ -167,23 +168,38 @@ def create_sse_app(
167
168
  # Get auth middleware from the provider
168
169
  auth_middleware = auth.get_middleware()
169
170
 
170
- # Get auth routes including protected MCP endpoint
171
- auth_routes = auth.get_routes(
172
- mcp_path=sse_path,
173
- mcp_endpoint=handle_sse,
174
- )
175
-
171
+ # Get auth provider's own routes (OAuth endpoints, metadata, etc)
172
+ auth_routes = auth.get_routes(mcp_path=sse_path)
176
173
  server_routes.extend(auth_routes)
177
174
  server_middleware.extend(auth_middleware)
178
175
 
179
- # Manually wrap the SSE message endpoint with RequireAuthMiddleware
176
+ # Build RFC 9728-compliant metadata URL
177
+ resource_url = auth._get_resource_url(sse_path)
178
+ resource_metadata_url = (
179
+ build_resource_metadata_url(resource_url) if resource_url else None
180
+ )
181
+
182
+ # Create protected SSE endpoint route with GET method only
183
+ server_routes.append(
184
+ Route(
185
+ sse_path,
186
+ endpoint=RequireAuthMiddleware(
187
+ handle_sse,
188
+ auth.required_scopes,
189
+ resource_metadata_url,
190
+ ),
191
+ methods=["GET"],
192
+ )
193
+ )
194
+
195
+ # Wrap the SSE message endpoint with RequireAuthMiddleware
180
196
  server_routes.append(
181
197
  Mount(
182
198
  message_path,
183
199
  app=RequireAuthMiddleware(
184
200
  sse.handle_post_message,
185
201
  auth.required_scopes,
186
- auth._get_resource_url("/.well-known/oauth-protected-resource"),
202
+ resource_metadata_url,
187
203
  ),
188
204
  )
189
205
  )
@@ -215,11 +231,17 @@ def create_sse_app(
215
231
  if middleware:
216
232
  server_middleware.extend(middleware)
217
233
 
234
+ @asynccontextmanager
235
+ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
236
+ async with server._lifespan_manager():
237
+ yield
238
+
218
239
  # Create and return the app
219
240
  app = create_base_app(
220
241
  routes=server_routes,
221
242
  middleware=server_middleware,
222
243
  debug=debug,
244
+ lifespan=lifespan,
223
245
  )
224
246
  # Store the FastMCP server instance on the Starlette app state
225
247
  app.state.fastmcp_server = server
@@ -274,14 +296,28 @@ def create_streamable_http_app(
274
296
  # Get auth middleware from the provider
275
297
  auth_middleware = auth.get_middleware()
276
298
 
277
- # Get auth routes including protected MCP endpoint
278
- auth_routes = auth.get_routes(
279
- mcp_path=streamable_http_path,
280
- mcp_endpoint=streamable_http_app,
281
- )
282
-
299
+ # Get auth provider's own routes (OAuth endpoints, metadata, etc)
300
+ auth_routes = auth.get_routes(mcp_path=streamable_http_path)
283
301
  server_routes.extend(auth_routes)
284
302
  server_middleware.extend(auth_middleware)
303
+
304
+ # Build RFC 9728-compliant metadata URL
305
+ resource_url = auth._get_resource_url(streamable_http_path)
306
+ resource_metadata_url = (
307
+ build_resource_metadata_url(resource_url) if resource_url else None
308
+ )
309
+
310
+ # Create protected HTTP endpoint route
311
+ server_routes.append(
312
+ Route(
313
+ streamable_http_path,
314
+ endpoint=RequireAuthMiddleware(
315
+ streamable_http_app,
316
+ auth.required_scopes,
317
+ resource_metadata_url,
318
+ ),
319
+ )
320
+ )
285
321
  else:
286
322
  # No auth required
287
323
  server_routes.append(
@@ -303,8 +339,9 @@ def create_streamable_http_app(
303
339
  # Create a lifespan manager to start and stop the session manager
304
340
  @asynccontextmanager
305
341
  async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
306
- async with session_manager.run():
307
- yield
342
+ async with server._lifespan_manager():
343
+ async with session_manager.run():
344
+ yield
308
345
 
309
346
  # Create and return the app with lifespan
310
347
  app = create_base_app(