fastmcp 2.12.5__py3-none-any.whl → 2.13.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 (72) hide show
  1. fastmcp/cli/cli.py +7 -6
  2. fastmcp/cli/install/claude_code.py +6 -6
  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 +85 -171
  12. fastmcp/client/transports.py +77 -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/resources/types.py +30 -24
  24. fastmcp/server/auth/auth.py +40 -32
  25. fastmcp/server/auth/handlers/authorize.py +324 -0
  26. fastmcp/server/auth/jwt_issuer.py +236 -0
  27. fastmcp/server/auth/middleware.py +96 -0
  28. fastmcp/server/auth/oauth_proxy.py +1256 -242
  29. fastmcp/server/auth/oidc_proxy.py +23 -6
  30. fastmcp/server/auth/providers/auth0.py +40 -21
  31. fastmcp/server/auth/providers/aws.py +29 -3
  32. fastmcp/server/auth/providers/azure.py +178 -127
  33. fastmcp/server/auth/providers/descope.py +4 -6
  34. fastmcp/server/auth/providers/github.py +29 -8
  35. fastmcp/server/auth/providers/google.py +30 -9
  36. fastmcp/server/auth/providers/introspection.py +281 -0
  37. fastmcp/server/auth/providers/jwt.py +8 -2
  38. fastmcp/server/auth/providers/scalekit.py +179 -0
  39. fastmcp/server/auth/providers/supabase.py +172 -0
  40. fastmcp/server/auth/providers/workos.py +32 -14
  41. fastmcp/server/context.py +122 -36
  42. fastmcp/server/http.py +58 -18
  43. fastmcp/server/low_level.py +121 -2
  44. fastmcp/server/middleware/caching.py +469 -0
  45. fastmcp/server/middleware/error_handling.py +6 -2
  46. fastmcp/server/middleware/logging.py +48 -37
  47. fastmcp/server/middleware/middleware.py +28 -15
  48. fastmcp/server/middleware/rate_limiting.py +3 -3
  49. fastmcp/server/middleware/tool_injection.py +116 -0
  50. fastmcp/server/proxy.py +6 -6
  51. fastmcp/server/server.py +683 -207
  52. fastmcp/settings.py +24 -10
  53. fastmcp/tools/tool.py +7 -3
  54. fastmcp/tools/tool_manager.py +30 -112
  55. fastmcp/tools/tool_transform.py +3 -3
  56. fastmcp/utilities/cli.py +62 -22
  57. fastmcp/utilities/components.py +5 -0
  58. fastmcp/utilities/inspect.py +77 -21
  59. fastmcp/utilities/logging.py +118 -8
  60. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  61. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  62. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  63. fastmcp/utilities/tests.py +87 -4
  64. fastmcp/utilities/types.py +1 -1
  65. fastmcp/utilities/ui.py +617 -0
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
  68. fastmcp/cli/claude.py +0 -135
  69. fastmcp/utilities/storage.py +0 -204
  70. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
  71. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
  72. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.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,10 +41,12 @@ 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
48
48
  allowed_client_redirect_uris: list[str] | None = None
49
+ jwt_signing_key: str | None = None
49
50
 
50
51
  @field_validator("required_scopes", mode="before")
51
52
  @classmethod
@@ -166,11 +167,14 @@ class WorkOSProvider(OAuthProxy):
166
167
  client_secret: str | NotSetT = NotSet,
167
168
  authkit_domain: str | NotSetT = NotSet,
168
169
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
170
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
169
171
  redirect_path: str | NotSetT = NotSet,
170
172
  required_scopes: list[str] | None | NotSetT = NotSet,
171
173
  timeout_seconds: int | NotSetT = NotSet,
172
174
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
173
- client_storage: KVStorage | None = None,
175
+ client_storage: AsyncKeyValue | None = None,
176
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
177
+ require_authorization_consent: bool = True,
174
178
  ):
175
179
  """Initialize WorkOS OAuth provider.
176
180
 
@@ -178,14 +182,24 @@ class WorkOSProvider(OAuthProxy):
178
182
  client_id: WorkOS client ID
179
183
  client_secret: WorkOS client secret
180
184
  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)
185
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
186
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
187
+ to avoid 404s during discovery when mounting under a path.
182
188
  redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
183
189
  required_scopes: Required OAuth scopes (no default)
184
190
  timeout_seconds: HTTP request timeout for WorkOS API calls
185
191
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
186
192
  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.
193
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
194
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
195
+ disk store will be encrypted using a key derived from the JWT Signing Key.
196
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
197
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
198
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
199
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
200
+ When True, users see a consent screen before being redirected to WorkOS.
201
+ When False, authorization proceeds directly without user confirmation.
202
+ SECURITY WARNING: Only disable for local development or testing environments.
189
203
  """
190
204
 
191
205
  settings = WorkOSProviderSettings.model_validate(
@@ -196,10 +210,12 @@ class WorkOSProvider(OAuthProxy):
196
210
  "client_secret": client_secret,
197
211
  "authkit_domain": authkit_domain,
198
212
  "base_url": base_url,
213
+ "issuer_url": issuer_url,
199
214
  "redirect_path": redirect_path,
200
215
  "required_scopes": required_scopes,
201
216
  "timeout_seconds": timeout_seconds,
202
217
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
218
+ "jwt_signing_key": jwt_signing_key,
203
219
  }.items()
204
220
  if v is not NotSet
205
221
  }
@@ -249,12 +265,15 @@ class WorkOSProvider(OAuthProxy):
249
265
  token_verifier=token_verifier,
250
266
  base_url=settings.base_url,
251
267
  redirect_path=settings.redirect_path,
252
- issuer_url=settings.base_url,
268
+ issuer_url=settings.issuer_url
269
+ or settings.base_url, # Default to base_url if not specified
253
270
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
254
271
  client_storage=client_storage,
272
+ jwt_signing_key=settings.jwt_signing_key,
273
+ require_authorization_consent=require_authorization_consent,
255
274
  )
256
275
 
257
- logger.info(
276
+ logger.debug(
258
277
  "Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
259
278
  settings.client_id,
260
279
  authkit_domain_final,
@@ -264,7 +283,7 @@ class WorkOSProvider(OAuthProxy):
264
283
  class AuthKitProviderSettings(BaseSettings):
265
284
  model_config = SettingsConfigDict(
266
285
  env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
267
- env_file=".env",
286
+ env_file=ENV_FILE,
268
287
  extra="ignore",
269
288
  )
270
289
 
@@ -364,7 +383,6 @@ class AuthKitProvider(RemoteAuthProvider):
364
383
  def get_routes(
365
384
  self,
366
385
  mcp_path: str | None = None,
367
- mcp_endpoint: Any | None = None,
368
386
  ) -> list[Route]:
369
387
  """Get OAuth routes including AuthKit authorization server metadata forwarding.
370
388
 
@@ -373,10 +391,10 @@ class AuthKitProvider(RemoteAuthProvider):
373
391
 
374
392
  Args:
375
393
  mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
376
- mcp_endpoint: The MCP endpoint handler to protect with auth
394
+ This is used to advertise the resource URL in metadata.
377
395
  """
378
396
  # Get the standard protected resource routes from RemoteAuthProvider
379
- routes = super().get_routes(mcp_path, mcp_endpoint)
397
+ routes = super().get_routes(mcp_path)
380
398
 
381
399
  async def oauth_authorization_server_metadata(request):
382
400
  """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
@@ -20,6 +22,7 @@ from mcp.types import (
20
22
  AudioContent,
21
23
  ClientCapabilities,
22
24
  CreateMessageResult,
25
+ GetPromptResult,
23
26
  ImageContent,
24
27
  IncludeContext,
25
28
  ModelHint,
@@ -30,6 +33,8 @@ from mcp.types import (
30
33
  TextContent,
31
34
  )
32
35
  from mcp.types import CreateMessageRequestParams as SamplingParams
36
+ from mcp.types import Prompt as MCPPrompt
37
+ from mcp.types import Resource as MCPResource
33
38
  from pydantic.networks import AnyUrl
34
39
  from starlette.requests import Request
35
40
  from typing_extensions import TypeVar
@@ -44,14 +49,21 @@ from fastmcp.server.elicitation import (
44
49
  get_elicitation_schema,
45
50
  )
46
51
  from fastmcp.server.server import FastMCP
47
- from fastmcp.utilities.logging import get_logger
52
+ from fastmcp.utilities.logging import _clamp_logger, get_logger
48
53
  from fastmcp.utilities.types import get_cached_typeadapter
49
54
 
50
- logger = get_logger(__name__)
55
+ logger: Logger = get_logger(name=__name__)
56
+ to_client_logger: Logger = logger.getChild(suffix="to_client")
57
+
58
+ # Convert all levels of server -> client messages to debug level
59
+ # This clamp can be undone at runtime by calling `_unclamp_logger` or calling
60
+ # `_clamp_logger` with a different max level.
61
+ _clamp_logger(logger=to_client_logger, max_level="DEBUG")
62
+
51
63
 
52
64
  T = TypeVar("T", default=Any)
53
65
  _current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
54
- _flush_lock = asyncio.Lock()
66
+ _flush_lock = anyio.Lock()
55
67
 
56
68
 
57
69
  @dataclass
@@ -66,6 +78,18 @@ class LogData:
66
78
  extra: Mapping[str, Any] | None = None
67
79
 
68
80
 
81
+ _mcp_level_to_python_level = {
82
+ "debug": logging.DEBUG,
83
+ "info": logging.INFO,
84
+ "notice": logging.INFO,
85
+ "warning": logging.WARNING,
86
+ "error": logging.ERROR,
87
+ "critical": logging.CRITICAL,
88
+ "alert": logging.CRITICAL,
89
+ "emergency": logging.CRITICAL,
90
+ }
91
+
92
+
69
93
  @contextmanager
70
94
  def set_context(context: Context) -> Generator[Context, None, None]:
71
95
  token = _current_context.set(context)
@@ -194,6 +218,36 @@ class Context:
194
218
  related_request_id=self.request_id,
195
219
  )
196
220
 
221
+ async def list_resources(self) -> list[MCPResource]:
222
+ """List all available resources from the server.
223
+
224
+ Returns:
225
+ List of Resource objects available on the server
226
+ """
227
+ return await self.fastmcp._list_resources_mcp()
228
+
229
+ async def list_prompts(self) -> list[MCPPrompt]:
230
+ """List all available prompts from the server.
231
+
232
+ Returns:
233
+ List of Prompt objects available on the server
234
+ """
235
+ return await self.fastmcp._list_prompts_mcp()
236
+
237
+ async def get_prompt(
238
+ self, name: str, arguments: dict[str, Any] | None = None
239
+ ) -> GetPromptResult:
240
+ """Get a prompt by name with optional arguments.
241
+
242
+ Args:
243
+ name: The name of the prompt to get
244
+ arguments: Optional arguments to pass to the prompt
245
+
246
+ Returns:
247
+ The prompt result
248
+ """
249
+ return await self.fastmcp._get_prompt_mcp(name, arguments)
250
+
197
251
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
198
252
  """Read a resource by URI.
199
253
 
@@ -203,9 +257,7 @@ class Context:
203
257
  Returns:
204
258
  The resource content as either text or bytes
205
259
  """
206
- if self.fastmcp is None:
207
- raise ValueError("Context is not available outside of a request")
208
- return await self.fastmcp._mcp_read_resource(uri)
260
+ return await self.fastmcp._read_resource_mcp(uri)
209
261
 
210
262
  async def log(
211
263
  self,
@@ -216,6 +268,8 @@ class Context:
216
268
  ) -> None:
217
269
  """Send a log message to the client.
218
270
 
271
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
272
+
219
273
  Args:
220
274
  message: Log message
221
275
  level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
@@ -223,13 +277,13 @@ class Context:
223
277
  logger_name: Optional logger name
224
278
  extra: Optional mapping for additional arguments
225
279
  """
226
- if level is None:
227
- level = "info"
228
280
  data = LogData(msg=message, extra=extra)
229
- await self.session.send_log_message(
230
- level=level,
281
+
282
+ await _log_to_server_and_client(
231
283
  data=data,
232
- logger=logger_name,
284
+ session=self.session,
285
+ level=level or "info",
286
+ logger_name=logger_name,
233
287
  related_request_id=self.request_id,
234
288
  )
235
289
 
@@ -303,9 +357,14 @@ class Context:
303
357
  logger_name: str | None = None,
304
358
  extra: Mapping[str, Any] | None = None,
305
359
  ) -> None:
306
- """Send a debug log message."""
360
+ """Send a `DEBUG`-level message to the connected MCP Client.
361
+
362
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
307
363
  await self.log(
308
- level="debug", message=message, logger_name=logger_name, extra=extra
364
+ level="debug",
365
+ message=message,
366
+ logger_name=logger_name,
367
+ extra=extra,
309
368
  )
310
369
 
311
370
  async def info(
@@ -314,9 +373,14 @@ class Context:
314
373
  logger_name: str | None = None,
315
374
  extra: Mapping[str, Any] | None = None,
316
375
  ) -> None:
317
- """Send an info log message."""
376
+ """Send a `INFO`-level message to the connected MCP Client.
377
+
378
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
318
379
  await self.log(
319
- level="info", message=message, logger_name=logger_name, extra=extra
380
+ level="info",
381
+ message=message,
382
+ logger_name=logger_name,
383
+ extra=extra,
320
384
  )
321
385
 
322
386
  async def warning(
@@ -325,9 +389,14 @@ class Context:
325
389
  logger_name: str | None = None,
326
390
  extra: Mapping[str, Any] | None = None,
327
391
  ) -> None:
328
- """Send a warning log message."""
392
+ """Send a `WARNING`-level message to the connected MCP Client.
393
+
394
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
329
395
  await self.log(
330
- level="warning", message=message, logger_name=logger_name, extra=extra
396
+ level="warning",
397
+ message=message,
398
+ logger_name=logger_name,
399
+ extra=extra,
331
400
  )
332
401
 
333
402
  async def error(
@@ -336,9 +405,14 @@ class Context:
336
405
  logger_name: str | None = None,
337
406
  extra: Mapping[str, Any] | None = None,
338
407
  ) -> None:
339
- """Send an error log message."""
408
+ """Send a `ERROR`-level message to the connected MCP Client.
409
+
410
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
340
411
  await self.log(
341
- level="error", message=message, logger_name=logger_name, extra=extra
412
+ level="error",
413
+ message=message,
414
+ logger_name=logger_name,
415
+ extra=extra,
342
416
  )
343
417
 
344
418
  async def list_roots(self) -> list[Root]:
@@ -592,30 +666,14 @@ class Context:
592
666
  def _queue_tool_list_changed(self) -> None:
593
667
  """Queue a tool list changed notification."""
594
668
  self._notification_queue.add("notifications/tools/list_changed")
595
- self._try_flush_notifications()
596
669
 
597
670
  def _queue_resource_list_changed(self) -> None:
598
671
  """Queue a resource list changed notification."""
599
672
  self._notification_queue.add("notifications/resources/list_changed")
600
- self._try_flush_notifications()
601
673
 
602
674
  def _queue_prompt_list_changed(self) -> None:
603
675
  """Queue a prompt list changed notification."""
604
676
  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
677
 
620
678
  async def _flush_notifications(self) -> None:
621
679
  """Send all queued notifications."""
@@ -675,3 +733,31 @@ def _parse_model_preferences(
675
733
  raise ValueError(
676
734
  "model_preferences must be one of: ModelPreferences, str, list[str], or None."
677
735
  )
736
+
737
+
738
+ async def _log_to_server_and_client(
739
+ data: LogData,
740
+ session: ServerSession,
741
+ level: LoggingLevel,
742
+ logger_name: str | None = None,
743
+ related_request_id: str | None = None,
744
+ ) -> None:
745
+ """Log a message to the server and client."""
746
+
747
+ msg_prefix = f"Sending {level.upper()} to client"
748
+
749
+ if logger_name:
750
+ msg_prefix += f" ({logger_name})"
751
+
752
+ to_client_logger.log(
753
+ level=_mcp_level_to_python_level[level],
754
+ msg=f"{msg_prefix}: {data.msg}",
755
+ extra=data.extra,
756
+ )
757
+
758
+ await session.send_log_message(
759
+ level=level,
760
+ data=data,
761
+ logger=logger_name,
762
+ related_request_id=related_request_id,
763
+ )