fastmcp 2.12.5__py3-none-any.whl → 2.14.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 (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  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 +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -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 +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/context.py CHANGED
@@ -1,57 +1,64 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import copy
5
4
  import inspect
6
- import warnings
5
+ import logging
7
6
  import weakref
8
7
  from collections.abc import Generator, Mapping, Sequence
9
8
  from contextlib import contextmanager
10
9
  from contextvars import ContextVar, Token
11
10
  from dataclasses import dataclass
12
- from enum import Enum
13
- from typing import Any, Literal, cast, get_origin, overload
11
+ from logging import Logger
12
+ from typing import Any, overload
14
13
 
14
+ import anyio
15
15
  from mcp import LoggingLevel, ServerSession
16
16
  from mcp.server.lowlevel.helper_types import ReadResourceContents
17
17
  from mcp.server.lowlevel.server import request_ctx
18
18
  from mcp.shared.context import RequestContext
19
19
  from mcp.types import (
20
- AudioContent,
21
20
  ClientCapabilities,
22
21
  CreateMessageResult,
23
- ImageContent,
22
+ GetPromptResult,
24
23
  IncludeContext,
25
24
  ModelHint,
26
25
  ModelPreferences,
27
26
  Root,
28
27
  SamplingCapability,
29
28
  SamplingMessage,
29
+ SamplingMessageContentBlock,
30
30
  TextContent,
31
31
  )
32
32
  from mcp.types import CreateMessageRequestParams as SamplingParams
33
+ from mcp.types import Prompt as MCPPrompt
34
+ from mcp.types import Resource as MCPResource
33
35
  from pydantic.networks import AnyUrl
34
36
  from starlette.requests import Request
35
37
  from typing_extensions import TypeVar
36
38
 
37
- import fastmcp.server.dependencies
38
- from fastmcp import settings
39
39
  from fastmcp.server.elicitation import (
40
40
  AcceptedElicitation,
41
41
  CancelledElicitation,
42
42
  DeclinedElicitation,
43
- ScalarElicitationType,
44
- get_elicitation_schema,
43
+ handle_elicit_accept,
44
+ parse_elicit_response_type,
45
45
  )
46
46
  from fastmcp.server.server import FastMCP
47
- from fastmcp.utilities.logging import get_logger
48
- from fastmcp.utilities.types import get_cached_typeadapter
47
+ from fastmcp.utilities.logging import _clamp_logger, get_logger
48
+
49
+ logger: Logger = get_logger(name=__name__)
50
+ to_client_logger: Logger = logger.getChild(suffix="to_client")
51
+
52
+ # Convert all levels of server -> client messages to debug level
53
+ # This clamp can be undone at runtime by calling `_unclamp_logger` or calling
54
+ # `_clamp_logger` with a different max level.
55
+ _clamp_logger(logger=to_client_logger, max_level="DEBUG")
49
56
 
50
- logger = get_logger(__name__)
51
57
 
52
58
  T = TypeVar("T", default=Any)
59
+
53
60
  _current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
54
- _flush_lock = asyncio.Lock()
61
+ _flush_lock = anyio.Lock()
55
62
 
56
63
 
57
64
  @dataclass
@@ -66,6 +73,18 @@ class LogData:
66
73
  extra: Mapping[str, Any] | None = None
67
74
 
68
75
 
76
+ _mcp_level_to_python_level = {
77
+ "debug": logging.DEBUG,
78
+ "info": logging.INFO,
79
+ "notice": logging.INFO,
80
+ "warning": logging.WARNING,
81
+ "error": logging.ERROR,
82
+ "critical": logging.CRITICAL,
83
+ "alert": logging.CRITICAL,
84
+ "emergency": logging.CRITICAL,
85
+ }
86
+
87
+
69
88
  @contextmanager
70
89
  def set_context(context: Context) -> Generator[Context, None, None]:
71
90
  token = _current_context.set(context)
@@ -145,6 +164,12 @@ class Context:
145
164
  # Always set this context and save the token
146
165
  token = _current_context.set(self)
147
166
  self._tokens.append(token)
167
+
168
+ # Set current server for dependency injection (use weakref to avoid reference cycles)
169
+ from fastmcp.server.dependencies import _current_server
170
+
171
+ self._server_token = _current_server.set(weakref.ref(self.fastmcp))
172
+
148
173
  return self
149
174
 
150
175
  async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -152,20 +177,46 @@ class Context:
152
177
  # Flush any remaining notifications before exiting
153
178
  await self._flush_notifications()
154
179
 
180
+ # Reset server token
181
+ if hasattr(self, "_server_token"):
182
+ from fastmcp.server.dependencies import _current_server
183
+
184
+ _current_server.reset(self._server_token)
185
+ delattr(self, "_server_token")
186
+
187
+ # Reset context token
155
188
  if self._tokens:
156
189
  token = self._tokens.pop()
157
190
  _current_context.reset(token)
158
191
 
159
192
  @property
160
- def request_context(self) -> RequestContext[ServerSession, Any, Request]:
193
+ def request_context(self) -> RequestContext[ServerSession, Any, Request] | None:
161
194
  """Access to the underlying request context.
162
195
 
163
- If called outside of a request context, this will raise a ValueError.
196
+ Returns None when the MCP session has not been established yet.
197
+ Returns the full RequestContext once the MCP session is available.
198
+
199
+ For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
200
+ which works whether or not the MCP session is available.
201
+
202
+ Example in middleware:
203
+ ```python
204
+ async def on_request(self, context, call_next):
205
+ ctx = context.fastmcp_context
206
+ if ctx.request_context:
207
+ # MCP session available - can access session_id, request_id, etc.
208
+ session_id = ctx.session_id
209
+ else:
210
+ # MCP session not available yet - use HTTP helpers
211
+ from fastmcp.server.dependencies import get_http_request
212
+ request = get_http_request()
213
+ return await call_next(context)
214
+ ```
164
215
  """
165
216
  try:
166
217
  return request_ctx.get()
167
218
  except LookupError:
168
- raise ValueError("Context is not available outside of a request")
219
+ return None
169
220
 
170
221
  async def report_progress(
171
222
  self, progress: float, total: float | None = None, message: str | None = None
@@ -179,7 +230,7 @@ class Context:
179
230
 
180
231
  progress_token = (
181
232
  self.request_context.meta.progressToken
182
- if self.request_context.meta
233
+ if self.request_context and self.request_context.meta
183
234
  else None
184
235
  )
185
236
 
@@ -194,6 +245,36 @@ class Context:
194
245
  related_request_id=self.request_id,
195
246
  )
196
247
 
248
+ async def list_resources(self) -> list[MCPResource]:
249
+ """List all available resources from the server.
250
+
251
+ Returns:
252
+ List of Resource objects available on the server
253
+ """
254
+ return await self.fastmcp._list_resources_mcp()
255
+
256
+ async def list_prompts(self) -> list[MCPPrompt]:
257
+ """List all available prompts from the server.
258
+
259
+ Returns:
260
+ List of Prompt objects available on the server
261
+ """
262
+ return await self.fastmcp._list_prompts_mcp()
263
+
264
+ async def get_prompt(
265
+ self, name: str, arguments: dict[str, Any] | None = None
266
+ ) -> GetPromptResult:
267
+ """Get a prompt by name with optional arguments.
268
+
269
+ Args:
270
+ name: The name of the prompt to get
271
+ arguments: Optional arguments to pass to the prompt
272
+
273
+ Returns:
274
+ The prompt result
275
+ """
276
+ return await self.fastmcp._get_prompt_mcp(name, arguments)
277
+
197
278
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
198
279
  """Read a resource by URI.
199
280
 
@@ -203,9 +284,8 @@ class Context:
203
284
  Returns:
204
285
  The resource content as either text or bytes
205
286
  """
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)
287
+ # Context calls don't have task metadata, so always returns list
288
+ return await self.fastmcp._read_resource_mcp(uri) # type: ignore[return-value]
209
289
 
210
290
  async def log(
211
291
  self,
@@ -216,6 +296,8 @@ class Context:
216
296
  ) -> None:
217
297
  """Send a log message to the client.
218
298
 
299
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
300
+
219
301
  Args:
220
302
  message: Log message
221
303
  level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
@@ -223,13 +305,13 @@ class Context:
223
305
  logger_name: Optional logger name
224
306
  extra: Optional mapping for additional arguments
225
307
  """
226
- if level is None:
227
- level = "info"
228
308
  data = LogData(msg=message, extra=extra)
229
- await self.session.send_log_message(
230
- level=level,
309
+
310
+ await _log_to_server_and_client(
231
311
  data=data,
232
- logger=logger_name,
312
+ session=self.session,
313
+ level=level or "info",
314
+ logger_name=logger_name,
233
315
  related_request_id=self.request_id,
234
316
  )
235
317
 
@@ -238,13 +320,21 @@ class Context:
238
320
  """Get the client ID if available."""
239
321
  return (
240
322
  getattr(self.request_context.meta, "client_id", None)
241
- if self.request_context.meta
323
+ if self.request_context and self.request_context.meta
242
324
  else None
243
325
  )
244
326
 
245
327
  @property
246
328
  def request_id(self) -> str:
247
- """Get the unique ID for this request."""
329
+ """Get the unique ID for this request.
330
+
331
+ Raises RuntimeError if MCP request context is not available.
332
+ """
333
+ if self.request_context is None:
334
+ raise RuntimeError(
335
+ "request_id is not available because the MCP session has not been established yet. "
336
+ "Check `context.request_context` for None before accessing this attribute."
337
+ )
248
338
  return str(self.request_context.request_id)
249
339
 
250
340
  @property
@@ -259,6 +349,9 @@ class Context:
259
349
  The session ID for StreamableHTTP transports, or a generated ID
260
350
  for other transports.
261
351
 
352
+ Raises:
353
+ RuntimeError if MCP request context is not available.
354
+
262
355
  Example:
263
356
  ```python
264
357
  @server.tool
@@ -269,6 +362,11 @@ class Context:
269
362
  ```
270
363
  """
271
364
  request_ctx = self.request_context
365
+ if request_ctx is None:
366
+ raise RuntimeError(
367
+ "session_id is not available because the MCP session has not been established yet. "
368
+ "Check `context.request_context` for None before accessing this attribute."
369
+ )
272
370
  session = request_ctx.session
273
371
 
274
372
  # Try to get the session ID from the session attributes
@@ -288,12 +386,20 @@ class Context:
288
386
  session_id = str(uuid4())
289
387
 
290
388
  # Save the session id to the session attributes
291
- setattr(session, "_fastmcp_id", session_id)
389
+ session._fastmcp_id = session_id # type: ignore[attr-defined]
292
390
  return session_id
293
391
 
294
392
  @property
295
393
  def session(self) -> ServerSession:
296
- """Access to the underlying session for advanced usage."""
394
+ """Access to the underlying session for advanced usage.
395
+
396
+ Raises RuntimeError if MCP request context is not available.
397
+ """
398
+ if self.request_context is None:
399
+ raise RuntimeError(
400
+ "session is not available because the MCP session has not been established yet. "
401
+ "Check `context.request_context` for None before accessing this attribute."
402
+ )
297
403
  return self.request_context.session
298
404
 
299
405
  # Convenience methods for common log levels
@@ -303,9 +409,14 @@ class Context:
303
409
  logger_name: str | None = None,
304
410
  extra: Mapping[str, Any] | None = None,
305
411
  ) -> None:
306
- """Send a debug log message."""
412
+ """Send a `DEBUG`-level message to the connected MCP Client.
413
+
414
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
307
415
  await self.log(
308
- level="debug", message=message, logger_name=logger_name, extra=extra
416
+ level="debug",
417
+ message=message,
418
+ logger_name=logger_name,
419
+ extra=extra,
309
420
  )
310
421
 
311
422
  async def info(
@@ -314,9 +425,14 @@ class Context:
314
425
  logger_name: str | None = None,
315
426
  extra: Mapping[str, Any] | None = None,
316
427
  ) -> None:
317
- """Send an info log message."""
428
+ """Send a `INFO`-level message to the connected MCP Client.
429
+
430
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
318
431
  await self.log(
319
- level="info", message=message, logger_name=logger_name, extra=extra
432
+ level="info",
433
+ message=message,
434
+ logger_name=logger_name,
435
+ extra=extra,
320
436
  )
321
437
 
322
438
  async def warning(
@@ -325,9 +441,14 @@ class Context:
325
441
  logger_name: str | None = None,
326
442
  extra: Mapping[str, Any] | None = None,
327
443
  ) -> None:
328
- """Send a warning log message."""
444
+ """Send a `WARNING`-level message to the connected MCP Client.
445
+
446
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
329
447
  await self.log(
330
- level="warning", message=message, logger_name=logger_name, extra=extra
448
+ level="warning",
449
+ message=message,
450
+ logger_name=logger_name,
451
+ extra=extra,
331
452
  )
332
453
 
333
454
  async def error(
@@ -336,9 +457,14 @@ class Context:
336
457
  logger_name: str | None = None,
337
458
  extra: Mapping[str, Any] | None = None,
338
459
  ) -> None:
339
- """Send an error log message."""
460
+ """Send a `ERROR`-level message to the connected MCP Client.
461
+
462
+ Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
340
463
  await self.log(
341
- level="error", message=message, logger_name=logger_name, extra=extra
464
+ level="error",
465
+ message=message,
466
+ logger_name=logger_name,
467
+ extra=extra,
342
468
  )
343
469
 
344
470
  async def list_roots(self) -> list[Root]:
@@ -358,6 +484,45 @@ class Context:
358
484
  """Send a prompt list changed notification to the client."""
359
485
  await self.session.send_prompt_list_changed()
360
486
 
487
+ async def close_sse_stream(self) -> None:
488
+ """Close the current response stream to trigger client reconnection.
489
+
490
+ When using StreamableHTTP transport with an EventStore configured, this
491
+ method gracefully closes the HTTP connection for the current request.
492
+ The client will automatically reconnect (after `retry_interval` milliseconds)
493
+ and resume receiving events from where it left off via the EventStore.
494
+
495
+ This is useful for long-running operations to avoid load balancer timeouts.
496
+ Instead of holding a connection open for minutes, you can periodically close
497
+ and let the client reconnect.
498
+
499
+ Example:
500
+ ```python
501
+ @mcp.tool
502
+ async def long_running_task(ctx: Context) -> str:
503
+ for i in range(100):
504
+ await ctx.report_progress(i, 100)
505
+
506
+ # Close connection every 30 iterations to avoid LB timeouts
507
+ if i % 30 == 0 and i > 0:
508
+ await ctx.close_sse_stream()
509
+
510
+ await do_work()
511
+ return "Done"
512
+ ```
513
+
514
+ Note:
515
+ This is a no-op (with a debug log) if not using StreamableHTTP
516
+ transport with an EventStore configured.
517
+ """
518
+ if not self.request_context or not self.request_context.close_sse_stream:
519
+ logger.debug(
520
+ "close_sse_stream() called but not applicable "
521
+ "(requires StreamableHTTP transport with event_store)"
522
+ )
523
+ return
524
+ await self.request_context.close_sse_stream()
525
+
361
526
  async def sample(
362
527
  self,
363
528
  messages: str | Sequence[str | SamplingMessage],
@@ -366,7 +531,7 @@ class Context:
366
531
  temperature: float | None = None,
367
532
  max_tokens: int | None = None,
368
533
  model_preferences: ModelPreferences | str | list[str] | None = None,
369
- ) -> TextContent | ImageContent | AudioContent:
534
+ ) -> SamplingMessageContentBlock | list[SamplingMessageContentBlock]:
370
535
  """
371
536
  Send a sampling request to the client and await the response.
372
537
 
@@ -412,7 +577,7 @@ class Context:
412
577
  maxTokens=max_tokens,
413
578
  modelPreferences=_parse_model_preferences(model_preferences),
414
579
  ),
415
- self.request_context,
580
+ self.request_context, # type: ignore[arg-type]
416
581
  )
417
582
 
418
583
  if inspect.isawaitable(create_message_result):
@@ -507,80 +672,23 @@ class Context:
507
672
  type or dataclass or BaseModel. If it is a primitive type, an
508
673
  object schema with a single "value" field will be generated.
509
674
  """
510
- if response_type is None:
511
- schema = {"type": "object", "properties": {}}
512
- else:
513
- # if the user provided a list of strings, treat it as a Literal
514
- if isinstance(response_type, list):
515
- if not all(isinstance(item, str) for item in response_type):
516
- raise ValueError(
517
- "List of options must be a list of strings. Received: "
518
- f"{response_type}"
519
- )
520
- # Convert list of options to Literal type and wrap
521
- choice_literal = Literal[tuple(response_type)] # type: ignore
522
- response_type = ScalarElicitationType[choice_literal] # type: ignore
523
- # if the user provided a primitive scalar, wrap it in an object schema
524
- elif response_type in {bool, int, float, str}:
525
- response_type = ScalarElicitationType[response_type] # type: ignore
526
- # if the user provided a Literal type, wrap it in an object schema
527
- elif get_origin(response_type) is Literal:
528
- response_type = ScalarElicitationType[response_type] # type: ignore
529
- # if the user provided an Enum type, wrap it in an object schema
530
- elif isinstance(response_type, type) and issubclass(response_type, Enum):
531
- response_type = ScalarElicitationType[response_type] # type: ignore
532
-
533
- response_type = cast(type[T], response_type)
534
-
535
- schema = get_elicitation_schema(response_type)
675
+ config = parse_elicit_response_type(response_type)
536
676
 
537
677
  result = await self.session.elicit(
538
678
  message=message,
539
- requestedSchema=schema,
679
+ requestedSchema=config.schema,
540
680
  related_request_id=self.request_id,
541
681
  )
542
682
 
543
683
  if result.action == "accept":
544
- if response_type is not None:
545
- type_adapter = get_cached_typeadapter(response_type)
546
- validated_data = cast(
547
- T | ScalarElicitationType[T],
548
- type_adapter.validate_python(result.content),
549
- )
550
- if isinstance(validated_data, ScalarElicitationType):
551
- return AcceptedElicitation[T](data=validated_data.value)
552
- else:
553
- return AcceptedElicitation[T](data=cast(T, validated_data))
554
- elif result.content:
555
- raise ValueError(
556
- "Elicitation expected an empty response, but received: "
557
- f"{result.content}"
558
- )
559
- else:
560
- return AcceptedElicitation[dict[str, Any]](data={})
684
+ return handle_elicit_accept(config, result.content)
561
685
  elif result.action == "decline":
562
686
  return DeclinedElicitation()
563
687
  elif result.action == "cancel":
564
688
  return CancelledElicitation()
565
689
  else:
566
- # This should never happen, but handle it just in case
567
690
  raise ValueError(f"Unexpected elicitation action: {result.action}")
568
691
 
569
- def get_http_request(self) -> Request:
570
- """Get the active starlette request."""
571
-
572
- # Deprecated in 2.2.11
573
- if settings.deprecation_warnings:
574
- warnings.warn(
575
- "Context.get_http_request() is deprecated and will be removed in a future version. "
576
- "Use get_http_request() from fastmcp.server.dependencies instead. "
577
- "See https://gofastmcp.com/servers/context#http-requests for more details.",
578
- DeprecationWarning,
579
- stacklevel=2,
580
- )
581
-
582
- return fastmcp.server.dependencies.get_http_request()
583
-
584
692
  def set_state(self, key: str, value: Any) -> None:
585
693
  """Set a value in the context state."""
586
694
  self._state[key] = value
@@ -592,30 +700,14 @@ class Context:
592
700
  def _queue_tool_list_changed(self) -> None:
593
701
  """Queue a tool list changed notification."""
594
702
  self._notification_queue.add("notifications/tools/list_changed")
595
- self._try_flush_notifications()
596
703
 
597
704
  def _queue_resource_list_changed(self) -> None:
598
705
  """Queue a resource list changed notification."""
599
706
  self._notification_queue.add("notifications/resources/list_changed")
600
- self._try_flush_notifications()
601
707
 
602
708
  def _queue_prompt_list_changed(self) -> None:
603
709
  """Queue a prompt list changed notification."""
604
710
  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
711
 
620
712
  async def _flush_notifications(self) -> None:
621
713
  """Send all queued notifications."""
@@ -675,3 +767,31 @@ def _parse_model_preferences(
675
767
  raise ValueError(
676
768
  "model_preferences must be one of: ModelPreferences, str, list[str], or None."
677
769
  )
770
+
771
+
772
+ async def _log_to_server_and_client(
773
+ data: LogData,
774
+ session: ServerSession,
775
+ level: LoggingLevel,
776
+ logger_name: str | None = None,
777
+ related_request_id: str | None = None,
778
+ ) -> None:
779
+ """Log a message to the server and client."""
780
+
781
+ msg_prefix = f"Sending {level.upper()} to client"
782
+
783
+ if logger_name:
784
+ msg_prefix += f" ({logger_name})"
785
+
786
+ to_client_logger.log(
787
+ level=_mcp_level_to_python_level[level],
788
+ msg=f"{msg_prefix}: {data.msg}",
789
+ extra=data.extra,
790
+ )
791
+
792
+ await session.send_log_message(
793
+ level=level,
794
+ data=data,
795
+ logger=logger_name,
796
+ related_request_id=related_request_id,
797
+ )