fastmcp 2.12.1__py3-none-any.whl → 2.13.2__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 (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/context.py CHANGED
@@ -1,25 +1,29 @@
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
- from collections.abc import Generator, Mapping
8
+ from collections.abc import Generator, Mapping, Sequence
9
9
  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
18
20
  from mcp.shared.context import RequestContext
19
21
  from mcp.types import (
22
+ AudioContent,
20
23
  ClientCapabilities,
21
- ContentBlock,
22
24
  CreateMessageResult,
25
+ GetPromptResult,
26
+ ImageContent,
23
27
  IncludeContext,
24
28
  ModelHint,
25
29
  ModelPreferences,
@@ -29,6 +33,8 @@ from mcp.types import (
29
33
  TextContent,
30
34
  )
31
35
  from mcp.types import CreateMessageRequestParams as SamplingParams
36
+ from mcp.types import Prompt as MCPPrompt
37
+ from mcp.types import Resource as MCPResource
32
38
  from pydantic.networks import AnyUrl
33
39
  from starlette.requests import Request
34
40
  from typing_extensions import TypeVar
@@ -43,14 +49,21 @@ from fastmcp.server.elicitation import (
43
49
  get_elicitation_schema,
44
50
  )
45
51
  from fastmcp.server.server import FastMCP
46
- from fastmcp.utilities.logging import get_logger
52
+ from fastmcp.utilities.logging import _clamp_logger, get_logger
47
53
  from fastmcp.utilities.types import get_cached_typeadapter
48
54
 
49
- 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
+
50
63
 
51
64
  T = TypeVar("T", default=Any)
52
65
  _current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
53
- _flush_lock = asyncio.Lock()
66
+ _flush_lock = anyio.Lock()
54
67
 
55
68
 
56
69
  @dataclass
@@ -65,6 +78,18 @@ class LogData:
65
78
  extra: Mapping[str, Any] | None = None
66
79
 
67
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
+
68
93
  @contextmanager
69
94
  def set_context(context: Context) -> Generator[Context, None, None]:
70
95
  token = _current_context.set(context)
@@ -85,18 +110,18 @@ class Context:
85
110
 
86
111
  ```python
87
112
  @server.tool
88
- def my_tool(x: int, ctx: Context) -> str:
113
+ async def my_tool(x: int, ctx: Context) -> str:
89
114
  # Log messages to the client
90
- ctx.info(f"Processing {x}")
91
- ctx.debug("Debug info")
92
- ctx.warning("Warning message")
93
- ctx.error("Error message")
115
+ await ctx.info(f"Processing {x}")
116
+ await ctx.debug("Debug info")
117
+ await ctx.warning("Warning message")
118
+ await ctx.error("Error message")
94
119
 
95
120
  # Report progress
96
- ctx.report_progress(50, 100, "Processing")
121
+ await ctx.report_progress(50, 100, "Processing")
97
122
 
98
123
  # Access resources
99
- data = ctx.read_resource("resource://data")
124
+ data = await ctx.read_resource("resource://data")
100
125
 
101
126
  # Get request info
102
127
  request_id = ctx.request_id
@@ -156,15 +181,33 @@ class Context:
156
181
  _current_context.reset(token)
157
182
 
158
183
  @property
159
- def request_context(self) -> RequestContext[ServerSession, Any, Request]:
184
+ def request_context(self) -> RequestContext[ServerSession, Any, Request] | None:
160
185
  """Access to the underlying request context.
161
186
 
162
- If called outside of a request context, this will raise a ValueError.
187
+ Returns None when the MCP session has not been established yet.
188
+ Returns the full RequestContext once the MCP session is available.
189
+
190
+ For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
191
+ which works whether or not the MCP session is available.
192
+
193
+ Example in middleware:
194
+ ```python
195
+ async def on_request(self, context, call_next):
196
+ ctx = context.fastmcp_context
197
+ if ctx.request_context:
198
+ # MCP session available - can access session_id, request_id, etc.
199
+ session_id = ctx.session_id
200
+ else:
201
+ # MCP session not available yet - use HTTP helpers
202
+ from fastmcp.server.dependencies import get_http_request
203
+ request = get_http_request()
204
+ return await call_next(context)
205
+ ```
163
206
  """
164
207
  try:
165
208
  return request_ctx.get()
166
209
  except LookupError:
167
- raise ValueError("Context is not available outside of a request")
210
+ return None
168
211
 
169
212
  async def report_progress(
170
213
  self, progress: float, total: float | None = None, message: str | None = None
@@ -178,7 +221,7 @@ class Context:
178
221
 
179
222
  progress_token = (
180
223
  self.request_context.meta.progressToken
181
- if self.request_context.meta
224
+ if self.request_context and self.request_context.meta
182
225
  else None
183
226
  )
184
227
 
@@ -193,6 +236,36 @@ class Context:
193
236
  related_request_id=self.request_id,
194
237
  )
195
238
 
239
+ async def list_resources(self) -> list[MCPResource]:
240
+ """List all available resources from the server.
241
+
242
+ Returns:
243
+ List of Resource objects available on the server
244
+ """
245
+ return await self.fastmcp._list_resources_mcp()
246
+
247
+ async def list_prompts(self) -> list[MCPPrompt]:
248
+ """List all available prompts from the server.
249
+
250
+ Returns:
251
+ List of Prompt objects available on the server
252
+ """
253
+ return await self.fastmcp._list_prompts_mcp()
254
+
255
+ async def get_prompt(
256
+ self, name: str, arguments: dict[str, Any] | None = None
257
+ ) -> GetPromptResult:
258
+ """Get a prompt by name with optional arguments.
259
+
260
+ Args:
261
+ name: The name of the prompt to get
262
+ arguments: Optional arguments to pass to the prompt
263
+
264
+ Returns:
265
+ The prompt result
266
+ """
267
+ return await self.fastmcp._get_prompt_mcp(name, arguments)
268
+
196
269
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
197
270
  """Read a resource by URI.
198
271
 
@@ -202,9 +275,7 @@ class Context:
202
275
  Returns:
203
276
  The resource content as either text or bytes
204
277
  """
205
- if self.fastmcp is None:
206
- raise ValueError("Context is not available outside of a request")
207
- return await self.fastmcp._mcp_read_resource(uri)
278
+ return await self.fastmcp._read_resource_mcp(uri)
208
279
 
209
280
  async def log(
210
281
  self,
@@ -215,6 +286,8 @@ class Context:
215
286
  ) -> None:
216
287
  """Send a log message to the client.
217
288
 
289
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
290
+
218
291
  Args:
219
292
  message: Log message
220
293
  level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
@@ -222,13 +295,13 @@ class Context:
222
295
  logger_name: Optional logger name
223
296
  extra: Optional mapping for additional arguments
224
297
  """
225
- if level is None:
226
- level = "info"
227
298
  data = LogData(msg=message, extra=extra)
228
- await self.session.send_log_message(
229
- level=level,
299
+
300
+ await _log_to_server_and_client(
230
301
  data=data,
231
- logger=logger_name,
302
+ session=self.session,
303
+ level=level or "info",
304
+ logger_name=logger_name,
232
305
  related_request_id=self.request_id,
233
306
  )
234
307
 
@@ -237,13 +310,21 @@ class Context:
237
310
  """Get the client ID if available."""
238
311
  return (
239
312
  getattr(self.request_context.meta, "client_id", None)
240
- if self.request_context.meta
313
+ if self.request_context and self.request_context.meta
241
314
  else None
242
315
  )
243
316
 
244
317
  @property
245
318
  def request_id(self) -> str:
246
- """Get the unique ID for this request."""
319
+ """Get the unique ID for this request.
320
+
321
+ Raises RuntimeError if MCP request context is not available.
322
+ """
323
+ if self.request_context is None:
324
+ raise RuntimeError(
325
+ "request_id is not available because the MCP session has not been established yet. "
326
+ "Check `context.request_context` for None before accessing this attribute."
327
+ )
247
328
  return str(self.request_context.request_id)
248
329
 
249
330
  @property
@@ -258,6 +339,9 @@ class Context:
258
339
  The session ID for StreamableHTTP transports, or a generated ID
259
340
  for other transports.
260
341
 
342
+ Raises:
343
+ RuntimeError if MCP request context is not available.
344
+
261
345
  Example:
262
346
  ```python
263
347
  @server.tool
@@ -268,6 +352,11 @@ class Context:
268
352
  ```
269
353
  """
270
354
  request_ctx = self.request_context
355
+ if request_ctx is None:
356
+ raise RuntimeError(
357
+ "session_id is not available because the MCP session has not been established yet. "
358
+ "Check `context.request_context` for None before accessing this attribute."
359
+ )
271
360
  session = request_ctx.session
272
361
 
273
362
  # Try to get the session ID from the session attributes
@@ -287,12 +376,20 @@ class Context:
287
376
  session_id = str(uuid4())
288
377
 
289
378
  # Save the session id to the session attributes
290
- setattr(session, "_fastmcp_id", session_id)
379
+ session._fastmcp_id = session_id
291
380
  return session_id
292
381
 
293
382
  @property
294
383
  def session(self) -> ServerSession:
295
- """Access to the underlying session for advanced usage."""
384
+ """Access to the underlying session for advanced usage.
385
+
386
+ Raises RuntimeError if MCP request context is not available.
387
+ """
388
+ if self.request_context is None:
389
+ raise RuntimeError(
390
+ "session is not available because the MCP session has not been established yet. "
391
+ "Check `context.request_context` for None before accessing this attribute."
392
+ )
296
393
  return self.request_context.session
297
394
 
298
395
  # Convenience methods for common log levels
@@ -302,9 +399,14 @@ class Context:
302
399
  logger_name: str | None = None,
303
400
  extra: Mapping[str, Any] | None = None,
304
401
  ) -> None:
305
- """Send a debug log message."""
402
+ """Send a `DEBUG`-level message to the connected MCP Client.
403
+
404
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
306
405
  await self.log(
307
- level="debug", message=message, logger_name=logger_name, extra=extra
406
+ level="debug",
407
+ message=message,
408
+ logger_name=logger_name,
409
+ extra=extra,
308
410
  )
309
411
 
310
412
  async def info(
@@ -313,9 +415,14 @@ class Context:
313
415
  logger_name: str | None = None,
314
416
  extra: Mapping[str, Any] | None = None,
315
417
  ) -> None:
316
- """Send an info log message."""
418
+ """Send a `INFO`-level message to the connected MCP Client.
419
+
420
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
317
421
  await self.log(
318
- level="info", message=message, logger_name=logger_name, extra=extra
422
+ level="info",
423
+ message=message,
424
+ logger_name=logger_name,
425
+ extra=extra,
319
426
  )
320
427
 
321
428
  async def warning(
@@ -324,9 +431,14 @@ class Context:
324
431
  logger_name: str | None = None,
325
432
  extra: Mapping[str, Any] | None = None,
326
433
  ) -> None:
327
- """Send a warning log message."""
434
+ """Send a `WARNING`-level message to the connected MCP Client.
435
+
436
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
328
437
  await self.log(
329
- level="warning", message=message, logger_name=logger_name, extra=extra
438
+ level="warning",
439
+ message=message,
440
+ logger_name=logger_name,
441
+ extra=extra,
330
442
  )
331
443
 
332
444
  async def error(
@@ -335,9 +447,14 @@ class Context:
335
447
  logger_name: str | None = None,
336
448
  extra: Mapping[str, Any] | None = None,
337
449
  ) -> None:
338
- """Send an error log message."""
450
+ """Send a `ERROR`-level message to the connected MCP Client.
451
+
452
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
339
453
  await self.log(
340
- level="error", message=message, logger_name=logger_name, extra=extra
454
+ level="error",
455
+ message=message,
456
+ logger_name=logger_name,
457
+ extra=extra,
341
458
  )
342
459
 
343
460
  async def list_roots(self) -> list[Root]:
@@ -359,13 +476,13 @@ class Context:
359
476
 
360
477
  async def sample(
361
478
  self,
362
- messages: str | list[str | SamplingMessage],
479
+ messages: str | Sequence[str | SamplingMessage],
363
480
  system_prompt: str | None = None,
364
481
  include_context: IncludeContext | None = None,
365
482
  temperature: float | None = None,
366
483
  max_tokens: int | None = None,
367
484
  model_preferences: ModelPreferences | str | list[str] | None = None,
368
- ) -> ContentBlock:
485
+ ) -> TextContent | ImageContent | AudioContent:
369
486
  """
370
487
  Send a sampling request to the client and await the response.
371
488
 
@@ -383,7 +500,7 @@ class Context:
383
500
  content=TextContent(text=messages, type="text"), role="user"
384
501
  )
385
502
  ]
386
- elif isinstance(messages, list):
503
+ elif isinstance(messages, Sequence):
387
504
  sampling_messages = [
388
505
  SamplingMessage(content=TextContent(text=m, type="text"), role="user")
389
506
  if isinstance(m, str)
@@ -449,7 +566,7 @@ class Context:
449
566
  AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
450
567
  ): ...
451
568
 
452
- """When response_type is None, the accepted elicitaiton will contain an
569
+ """When response_type is None, the accepted elicitation will contain an
453
570
  empty dict"""
454
571
 
455
572
  @overload
@@ -459,7 +576,7 @@ class Context:
459
576
  response_type: type[T],
460
577
  ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ...
461
578
 
462
- """When response_type is not None, the accepted elicitaiton will contain the
579
+ """When response_type is not None, the accepted elicitation will contain the
463
580
  response data"""
464
581
 
465
582
  @overload
@@ -469,7 +586,7 @@ class Context:
469
586
  response_type: list[str],
470
587
  ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...
471
588
 
472
- """When response_type is a list of strings, the accepted elicitaiton will
589
+ """When response_type is a list of strings, the accepted elicitation will
473
590
  contain the selected string response"""
474
591
 
475
592
  async def elicit(
@@ -520,13 +637,11 @@ class Context:
520
637
  choice_literal = Literal[tuple(response_type)] # type: ignore
521
638
  response_type = ScalarElicitationType[choice_literal] # type: ignore
522
639
  # if the user provided a primitive scalar, wrap it in an object schema
523
- elif response_type in {bool, int, float, str}:
524
- response_type = ScalarElicitationType[response_type] # type: ignore
525
- # if the user provided a Literal type, wrap it in an object schema
526
- elif get_origin(response_type) is Literal:
527
- response_type = ScalarElicitationType[response_type] # type: ignore
528
- # if the user provided an Enum type, wrap it in an object schema
529
- elif isinstance(response_type, type) and issubclass(response_type, Enum):
640
+ elif (
641
+ response_type in {bool, int, float, str}
642
+ or get_origin(response_type) is Literal
643
+ or (isinstance(response_type, type) and issubclass(response_type, Enum))
644
+ ):
530
645
  response_type = ScalarElicitationType[response_type] # type: ignore
531
646
 
532
647
  response_type = cast(type[T], response_type)
@@ -573,7 +688,7 @@ class Context:
573
688
  warnings.warn(
574
689
  "Context.get_http_request() is deprecated and will be removed in a future version. "
575
690
  "Use get_http_request() from fastmcp.server.dependencies instead. "
576
- "See https://gofastmcp.com/patterns/http-requests for more details.",
691
+ "See https://gofastmcp.com/servers/context#http-requests for more details.",
577
692
  DeprecationWarning,
578
693
  stacklevel=2,
579
694
  )
@@ -591,30 +706,14 @@ class Context:
591
706
  def _queue_tool_list_changed(self) -> None:
592
707
  """Queue a tool list changed notification."""
593
708
  self._notification_queue.add("notifications/tools/list_changed")
594
- self._try_flush_notifications()
595
709
 
596
710
  def _queue_resource_list_changed(self) -> None:
597
711
  """Queue a resource list changed notification."""
598
712
  self._notification_queue.add("notifications/resources/list_changed")
599
- self._try_flush_notifications()
600
713
 
601
714
  def _queue_prompt_list_changed(self) -> None:
602
715
  """Queue a prompt list changed notification."""
603
716
  self._notification_queue.add("notifications/prompts/list_changed")
604
- self._try_flush_notifications()
605
-
606
- def _try_flush_notifications(self) -> None:
607
- """Synchronous method that attempts to flush notifications if we're in an async context."""
608
- try:
609
- # Check if we're in an async context
610
- loop = asyncio.get_running_loop()
611
- if loop and not loop.is_running():
612
- return
613
- # Schedule flush as a task (fire-and-forget)
614
- asyncio.create_task(self._flush_notifications())
615
- except RuntimeError:
616
- # No event loop - will flush later
617
- pass
618
717
 
619
718
  async def _flush_notifications(self) -> None:
620
719
  """Send all queued notifications."""
@@ -674,3 +773,31 @@ def _parse_model_preferences(
674
773
  raise ValueError(
675
774
  "model_preferences must be one of: ModelPreferences, str, list[str], or None."
676
775
  )
776
+
777
+
778
+ async def _log_to_server_and_client(
779
+ data: LogData,
780
+ session: ServerSession,
781
+ level: LoggingLevel,
782
+ logger_name: str | None = None,
783
+ related_request_id: str | None = None,
784
+ ) -> None:
785
+ """Log a message to the server and client."""
786
+
787
+ msg_prefix = f"Sending {level.upper()} to client"
788
+
789
+ if logger_name:
790
+ msg_prefix += f" ({logger_name})"
791
+
792
+ to_client_logger.log(
793
+ level=_mcp_level_to_python_level[level],
794
+ msg=f"{msg_prefix}: {data.msg}",
795
+ extra=data.extra,
796
+ )
797
+
798
+ await session.send_log_message(
799
+ level=level,
800
+ data=data,
801
+ logger=logger_name,
802
+ related_request_id=related_request_id,
803
+ )
@@ -1,23 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from mcp.server.auth.middleware.auth_context import (
6
7
  get_access_token as _sdk_get_access_token,
7
8
  )
9
+ from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
10
+ from mcp.server.auth.provider import (
11
+ AccessToken as _SDKAccessToken,
12
+ )
13
+ from mcp.server.lowlevel.server import request_ctx
8
14
  from starlette.requests import Request
9
15
 
10
16
  from fastmcp.server.auth import AccessToken
17
+ from fastmcp.server.http import _current_http_request
11
18
 
12
19
  if TYPE_CHECKING:
13
20
  from fastmcp.server.context import Context
14
21
 
15
22
  __all__ = [
23
+ "AccessToken",
24
+ "get_access_token",
16
25
  "get_context",
17
- "get_http_request",
18
26
  "get_http_headers",
19
- "get_access_token",
20
- "AccessToken",
27
+ "get_http_request",
21
28
  ]
22
29
 
23
30
 
@@ -37,13 +44,15 @@ def get_context() -> Context:
37
44
 
38
45
 
39
46
  def get_http_request() -> Request:
40
- from mcp.server.lowlevel.server import request_ctx
41
-
47
+ # Try MCP SDK's request_ctx first (set during normal MCP request handling)
42
48
  request = None
43
- try:
49
+ with contextlib.suppress(LookupError):
44
50
  request = request_ctx.get().request
45
- except LookupError:
46
- pass
51
+
52
+ # Fallback to FastMCP's HTTP context variable
53
+ # This is needed during `on_initialize` middleware where request_ctx isn't set yet
54
+ if request is None:
55
+ request = _current_http_request.get()
47
56
 
48
57
  if request is None:
49
58
  raise RuntimeError("No active HTTP request found.")
@@ -103,21 +112,52 @@ def get_access_token() -> AccessToken | None:
103
112
  """
104
113
  Get the FastMCP access token from the current context.
105
114
 
115
+ This function first tries to get the token from the current HTTP request's scope,
116
+ which is more reliable for long-lived connections where the SDK's auth_context_var
117
+ may become stale after token refresh. Falls back to the SDK's context var if no
118
+ request is available.
119
+
106
120
  Returns:
107
121
  The access token if an authenticated user is available, None otherwise.
108
122
  """
109
- #
110
- obj = _sdk_get_access_token()
111
- if obj is None or isinstance(obj, AccessToken):
112
- return obj
123
+ access_token: _SDKAccessToken | None = None
124
+
125
+ # First, try to get from current HTTP request's scope (issue #1863)
126
+ # This is more reliable than auth_context_var for Streamable HTTP sessions
127
+ # where tokens may be refreshed between MCP messages
128
+ try:
129
+ request = get_http_request()
130
+ user = request.scope.get("user")
131
+ if isinstance(user, AuthenticatedUser):
132
+ access_token = user.access_token
133
+ except RuntimeError:
134
+ # No HTTP request available, fall back to context var
135
+ pass
136
+
137
+ # Fall back to SDK's context var if we didn't get a token from the request
138
+ if access_token is None:
139
+ access_token = _sdk_get_access_token()
140
+
141
+ if access_token is None or isinstance(access_token, AccessToken):
142
+ return access_token
113
143
 
114
- # If the object is not a FastMCP AccessToken, convert it to one if the fields are compatible
115
- # This is a workaround for the case where the SDK returns a different type
144
+ # If the object is not a FastMCP AccessToken, convert it to one if the
145
+ # fields are compatible (e.g. `claims` is not present in the SDK's AccessToken).
146
+ # This is a workaround for the case where the SDK or auth provider returns a different type
116
147
  # If it fails, it will raise a TypeError
117
148
  try:
118
- return AccessToken(**obj.model_dump())
149
+ access_token_as_dict = access_token.model_dump()
150
+ return AccessToken(
151
+ token=access_token_as_dict["token"],
152
+ client_id=access_token_as_dict["client_id"],
153
+ scopes=access_token_as_dict["scopes"],
154
+ # Optional fields
155
+ expires_at=access_token_as_dict.get("expires_at"),
156
+ resource_owner=access_token_as_dict.get("resource_owner"),
157
+ claims=access_token_as_dict.get("claims"),
158
+ )
119
159
  except Exception as e:
120
160
  raise TypeError(
121
- f"Expected fastmcp.server.auth.auth.AccessToken, got {type(obj).__name__}. "
161
+ f"Expected fastmcp.server.auth.auth.AccessToken, got {type(access_token).__name__}. "
122
162
  "Ensure the SDK is using the correct AccessToken type."
123
163
  ) from e
@@ -20,8 +20,8 @@ __all__ = [
20
20
  "AcceptedElicitation",
21
21
  "CancelledElicitation",
22
22
  "DeclinedElicitation",
23
- "get_elicitation_schema",
24
23
  "ScalarElicitationType",
24
+ "get_elicitation_schema",
25
25
  ]
26
26
 
27
27
  logger = get_logger(__name__)