fastmcp 2.13.3__py3-none-any.whl → 2.14.1__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 (85) hide show
  1. fastmcp/__init__.py +0 -21
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +8 -22
  5. fastmcp/cli/install/shared.py +0 -15
  6. fastmcp/cli/tasks.py +110 -0
  7. fastmcp/client/auth/oauth.py +9 -9
  8. fastmcp/client/client.py +739 -136
  9. fastmcp/client/elicitation.py +11 -5
  10. fastmcp/client/messages.py +7 -5
  11. fastmcp/client/roots.py +2 -1
  12. fastmcp/client/sampling/__init__.py +69 -0
  13. fastmcp/client/sampling/handlers/__init__.py +0 -0
  14. fastmcp/client/sampling/handlers/anthropic.py +387 -0
  15. fastmcp/client/sampling/handlers/openai.py +399 -0
  16. fastmcp/client/tasks.py +551 -0
  17. fastmcp/client/transports.py +72 -21
  18. fastmcp/contrib/component_manager/component_service.py +4 -20
  19. fastmcp/dependencies.py +25 -0
  20. fastmcp/experimental/sampling/handlers/__init__.py +5 -0
  21. fastmcp/experimental/sampling/handlers/openai.py +4 -169
  22. fastmcp/experimental/server/openapi/__init__.py +15 -13
  23. fastmcp/experimental/utilities/openapi/__init__.py +12 -38
  24. fastmcp/prompts/prompt.py +38 -38
  25. fastmcp/resources/resource.py +33 -16
  26. fastmcp/resources/template.py +69 -59
  27. fastmcp/server/auth/__init__.py +0 -9
  28. fastmcp/server/auth/auth.py +127 -3
  29. fastmcp/server/auth/oauth_proxy.py +47 -97
  30. fastmcp/server/auth/oidc_proxy.py +7 -0
  31. fastmcp/server/auth/providers/in_memory.py +2 -2
  32. fastmcp/server/auth/providers/oci.py +2 -2
  33. fastmcp/server/context.py +509 -180
  34. fastmcp/server/dependencies.py +464 -6
  35. fastmcp/server/elicitation.py +285 -47
  36. fastmcp/server/event_store.py +177 -0
  37. fastmcp/server/http.py +15 -3
  38. fastmcp/server/low_level.py +56 -12
  39. fastmcp/server/middleware/middleware.py +2 -2
  40. fastmcp/server/openapi/__init__.py +35 -0
  41. fastmcp/{experimental/server → server}/openapi/components.py +4 -3
  42. fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
  43. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  44. fastmcp/server/proxy.py +53 -40
  45. fastmcp/server/sampling/__init__.py +10 -0
  46. fastmcp/server/sampling/run.py +301 -0
  47. fastmcp/server/sampling/sampling_tool.py +108 -0
  48. fastmcp/server/server.py +793 -552
  49. fastmcp/server/tasks/__init__.py +21 -0
  50. fastmcp/server/tasks/capabilities.py +22 -0
  51. fastmcp/server/tasks/config.py +89 -0
  52. fastmcp/server/tasks/converters.py +206 -0
  53. fastmcp/server/tasks/handlers.py +356 -0
  54. fastmcp/server/tasks/keys.py +93 -0
  55. fastmcp/server/tasks/protocol.py +355 -0
  56. fastmcp/server/tasks/subscriptions.py +205 -0
  57. fastmcp/settings.py +101 -103
  58. fastmcp/tools/tool.py +83 -49
  59. fastmcp/tools/tool_transform.py +1 -12
  60. fastmcp/utilities/components.py +3 -3
  61. fastmcp/utilities/json_schema_type.py +4 -4
  62. fastmcp/utilities/mcp_config.py +1 -2
  63. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  64. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  65. fastmcp/utilities/openapi/__init__.py +63 -0
  66. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  67. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
  68. fastmcp/utilities/tests.py +11 -5
  69. fastmcp/utilities/types.py +8 -0
  70. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/METADATA +7 -4
  71. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/RECORD +79 -63
  72. fastmcp/client/sampling.py +0 -56
  73. fastmcp/experimental/sampling/handlers/base.py +0 -21
  74. fastmcp/server/auth/providers/bearer.py +0 -25
  75. fastmcp/server/openapi.py +0 -1087
  76. fastmcp/server/sampling/handler.py +0 -19
  77. fastmcp/utilities/openapi.py +0 -1568
  78. /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  79. /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
  80. /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
  81. /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
  82. /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
  83. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/WHEEL +0 -0
  84. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/entry_points.txt +0 -0
  85. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextlib
4
- from typing import TYPE_CHECKING
4
+ import inspect
5
+ import weakref
6
+ from collections.abc import AsyncGenerator, Callable
7
+ from contextlib import AsyncExitStack, asynccontextmanager
8
+ from contextvars import ContextVar
9
+ from functools import lru_cache
10
+ from typing import TYPE_CHECKING, Any, cast, get_type_hints
5
11
 
12
+ from docket.dependencies import Dependency, _Depends, get_dependency_parameters
13
+ from docket.dependencies import Progress as DocketProgress
6
14
  from mcp.server.auth.middleware.auth_context import (
7
15
  get_access_token as _sdk_get_access_token,
8
16
  )
@@ -15,20 +23,233 @@ from starlette.requests import Request
15
23
 
16
24
  from fastmcp.server.auth import AccessToken
17
25
  from fastmcp.server.http import _current_http_request
26
+ from fastmcp.utilities.types import is_class_member_of_type
18
27
 
19
28
  if TYPE_CHECKING:
29
+ from docket import Docket
30
+ from docket.worker import Worker
31
+
20
32
  from fastmcp.server.context import Context
33
+ from fastmcp.server.server import FastMCP
34
+
35
+ # ContextVars for tracking Docket infrastructure
36
+ _current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None) # type: ignore[assignment]
37
+ _current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None) # type: ignore[assignment]
38
+ _current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar( # type: ignore[invalid-assignment]
39
+ "server", default=None
40
+ )
21
41
 
22
42
  __all__ = [
23
43
  "AccessToken",
44
+ "CurrentContext",
45
+ "CurrentDocket",
46
+ "CurrentFastMCP",
47
+ "CurrentWorker",
48
+ "Progress",
24
49
  "get_access_token",
25
50
  "get_context",
26
51
  "get_http_headers",
27
52
  "get_http_request",
53
+ "get_server",
54
+ "resolve_dependencies",
55
+ "without_injected_parameters",
28
56
  ]
29
57
 
30
58
 
31
- # --- Context ---
59
+ def _find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
60
+ """Find the name of the kwarg that is of type kwarg_type.
61
+
62
+ This is the legacy dependency injection approach, used specifically for
63
+ injecting the Context object when a function parameter is typed as Context.
64
+
65
+ Includes union types that contain the kwarg_type, as well as Annotated types.
66
+ """
67
+
68
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
69
+ fn = fn.__func__
70
+
71
+ try:
72
+ type_hints = get_type_hints(fn, include_extras=True)
73
+ except Exception:
74
+ type_hints = getattr(fn, "__annotations__", {})
75
+
76
+ sig = inspect.signature(fn)
77
+ for name, param in sig.parameters.items():
78
+ annotation = type_hints.get(name, param.annotation)
79
+ if is_class_member_of_type(annotation, kwarg_type):
80
+ return name
81
+ return None
82
+
83
+
84
+ @lru_cache(maxsize=5000)
85
+ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
86
+ """Create a wrapper function without injected parameters.
87
+
88
+ Returns a wrapper that excludes Context and Docket dependency parameters,
89
+ making it safe to use with Pydantic TypeAdapter for schema generation and
90
+ validation. The wrapper internally handles all dependency resolution and
91
+ Context injection when called.
92
+
93
+ Args:
94
+ fn: Original function with Context and/or dependencies
95
+
96
+ Returns:
97
+ Async wrapper function without injected parameters
98
+ """
99
+ from fastmcp.server.context import Context
100
+
101
+ # Identify parameters to exclude
102
+ context_kwarg = _find_kwarg_by_type(fn, Context)
103
+ dependency_params = get_dependency_parameters(fn)
104
+
105
+ exclude = set()
106
+ if context_kwarg:
107
+ exclude.add(context_kwarg)
108
+ if dependency_params:
109
+ exclude.update(dependency_params.keys())
110
+
111
+ if not exclude:
112
+ return fn
113
+
114
+ # Build new signature with only user parameters
115
+ sig = inspect.signature(fn)
116
+ user_params = [
117
+ param for name, param in sig.parameters.items() if name not in exclude
118
+ ]
119
+ new_sig = inspect.Signature(user_params)
120
+
121
+ # Create async wrapper that handles dependency resolution
122
+ async def wrapper(**user_kwargs: Any) -> Any:
123
+ async with resolve_dependencies(fn, user_kwargs) as resolved_kwargs:
124
+ result = fn(**resolved_kwargs)
125
+ if inspect.isawaitable(result):
126
+ result = await result
127
+ return result
128
+
129
+ # Set wrapper metadata (only parameter annotations, not return type)
130
+ wrapper.__signature__ = new_sig # type: ignore
131
+ wrapper.__annotations__ = {
132
+ k: v
133
+ for k, v in getattr(fn, "__annotations__", {}).items()
134
+ if k not in exclude and k != "return"
135
+ }
136
+ wrapper.__name__ = getattr(fn, "__name__", "wrapper")
137
+ wrapper.__doc__ = getattr(fn, "__doc__", None)
138
+
139
+ return wrapper
140
+
141
+
142
+ @asynccontextmanager
143
+ async def _resolve_fastmcp_dependencies(
144
+ fn: Callable[..., Any], arguments: dict[str, Any]
145
+ ) -> AsyncGenerator[dict[str, Any], None]:
146
+ """Resolve Docket dependencies for a FastMCP function.
147
+
148
+ Sets up the minimal context needed for Docket's Depends() to work:
149
+ - A cache for resolved dependencies
150
+ - An AsyncExitStack for managing context manager lifetimes
151
+
152
+ The Docket instance (for CurrentDocket dependency) is managed separately
153
+ by the server's lifespan and made available via ContextVar.
154
+
155
+ Note: This does NOT set up Docket's Execution context. If user code needs
156
+ Docket-specific dependencies like TaskArgument(), TaskKey(), etc., those
157
+ will fail with clear errors about missing context.
158
+
159
+ Args:
160
+ fn: The function to resolve dependencies for
161
+ arguments: The arguments passed to the function
162
+
163
+ Yields:
164
+ Dictionary of resolved dependencies merged with provided arguments
165
+ """
166
+ dependency_params = get_dependency_parameters(fn)
167
+
168
+ if not dependency_params:
169
+ yield arguments
170
+ return
171
+
172
+ # Initialize dependency cache and exit stack
173
+ cache_token = _Depends.cache.set({})
174
+ try:
175
+ async with AsyncExitStack() as stack:
176
+ stack_token = _Depends.stack.set(stack)
177
+ try:
178
+ resolved: dict[str, Any] = {}
179
+
180
+ for parameter, dependency in dependency_params.items():
181
+ # If argument was explicitly provided, use that instead
182
+ if parameter in arguments:
183
+ resolved[parameter] = arguments[parameter]
184
+ continue
185
+
186
+ # Resolve the dependency
187
+ try:
188
+ resolved[parameter] = await stack.enter_async_context(
189
+ dependency
190
+ )
191
+ except Exception as error:
192
+ fn_name = getattr(fn, "__name__", repr(fn))
193
+ raise RuntimeError(
194
+ f"Failed to resolve dependency '{parameter}' for {fn_name}"
195
+ ) from error
196
+
197
+ # Merge resolved dependencies with provided arguments
198
+ final_arguments = {**arguments, **resolved}
199
+
200
+ yield final_arguments
201
+ finally:
202
+ _Depends.stack.reset(stack_token)
203
+ finally:
204
+ _Depends.cache.reset(cache_token)
205
+
206
+
207
+ @asynccontextmanager
208
+ async def resolve_dependencies(
209
+ fn: Callable[..., Any], arguments: dict[str, Any]
210
+ ) -> AsyncGenerator[dict[str, Any], None]:
211
+ """Resolve dependencies and inject Context for a FastMCP function.
212
+
213
+ This function:
214
+ 1. Filters out any dependency parameter names from user arguments (security)
215
+ 2. Resolves Docket dependencies
216
+ 3. Injects Context if needed
217
+ 4. Merges everything together
218
+
219
+ The filtering prevents external callers from overriding injected parameters by
220
+ providing values for dependency parameter names. This is a security feature.
221
+
222
+ Args:
223
+ fn: The function to resolve dependencies for
224
+ arguments: User arguments (may contain keys that match dependency names,
225
+ which will be filtered out)
226
+
227
+ Yields:
228
+ Dictionary of filtered user args + resolved dependencies + Context
229
+
230
+ Example:
231
+ ```python
232
+ async with resolve_dependencies(my_tool, {"name": "Alice"}) as kwargs:
233
+ result = my_tool(**kwargs)
234
+ if inspect.isawaitable(result):
235
+ result = await result
236
+ ```
237
+ """
238
+ from fastmcp.server.context import Context
239
+
240
+ # Filter out dependency parameters from user arguments to prevent override
241
+ # This is a security measure - external callers should never be able to
242
+ # provide values for injected parameters
243
+ dependency_params = get_dependency_parameters(fn)
244
+ user_args = {k: v for k, v in arguments.items() if k not in dependency_params}
245
+
246
+ async with _resolve_fastmcp_dependencies(fn, user_args) as resolved_kwargs:
247
+ # Inject Context if needed
248
+ context_kwarg = _find_kwarg_by_type(fn, kwarg_type=Context)
249
+ if context_kwarg and context_kwarg not in resolved_kwargs:
250
+ resolved_kwargs[context_kwarg] = get_context()
251
+
252
+ yield resolved_kwargs
32
253
 
33
254
 
34
255
  def get_context() -> Context:
@@ -40,7 +261,247 @@ def get_context() -> Context:
40
261
  return context
41
262
 
42
263
 
43
- # --- HTTP Request ---
264
+ class _CurrentContext(Dependency):
265
+ """Internal dependency class for CurrentContext."""
266
+
267
+ async def __aenter__(self) -> Context:
268
+ return get_context()
269
+
270
+
271
+ def CurrentContext() -> Context:
272
+ """Get the current FastMCP Context instance.
273
+
274
+ This dependency provides access to the active FastMCP Context for the
275
+ current MCP operation (tool/resource/prompt call).
276
+
277
+ Returns:
278
+ A dependency that resolves to the active Context instance
279
+
280
+ Raises:
281
+ RuntimeError: If no active context found (during resolution)
282
+
283
+ Example:
284
+ ```python
285
+ from fastmcp.dependencies import CurrentContext
286
+
287
+ @mcp.tool()
288
+ async def log_progress(ctx: Context = CurrentContext()) -> str:
289
+ ctx.report_progress(50, 100, "Halfway done")
290
+ return "Working"
291
+ ```
292
+ """
293
+ return cast("Context", _CurrentContext())
294
+
295
+
296
+ class _CurrentDocket(Dependency):
297
+ """Internal dependency class for CurrentDocket."""
298
+
299
+ async def __aenter__(self) -> Docket:
300
+ # Get Docket from ContextVar (set by _docket_lifespan)
301
+ docket = _current_docket.get()
302
+ if docket is None:
303
+ raise RuntimeError(
304
+ "No Docket instance found. Docket is only available within "
305
+ "a running FastMCP server context."
306
+ )
307
+
308
+ return docket
309
+
310
+
311
+ def CurrentDocket() -> Docket:
312
+ """Get the current Docket instance managed by FastMCP.
313
+
314
+ This dependency provides access to the Docket instance that FastMCP
315
+ automatically creates for background task scheduling.
316
+
317
+ Returns:
318
+ A dependency that resolves to the active Docket instance
319
+
320
+ Raises:
321
+ RuntimeError: If not within a FastMCP server context
322
+
323
+ Example:
324
+ ```python
325
+ from fastmcp.dependencies import CurrentDocket
326
+
327
+ @mcp.tool()
328
+ async def schedule_task(docket: Docket = CurrentDocket()) -> str:
329
+ await docket.add(some_function)(arg1, arg2)
330
+ return "Scheduled"
331
+ ```
332
+ """
333
+ return cast("Docket", _CurrentDocket())
334
+
335
+
336
+ class _CurrentWorker(Dependency):
337
+ """Internal dependency class for CurrentWorker."""
338
+
339
+ async def __aenter__(self) -> Worker:
340
+ worker = _current_worker.get()
341
+ if worker is None:
342
+ raise RuntimeError(
343
+ "No Worker instance found. Worker is only available within "
344
+ "a running FastMCP server context."
345
+ )
346
+
347
+ return worker
348
+
349
+
350
+ def CurrentWorker() -> Worker:
351
+ """Get the current Docket Worker instance managed by FastMCP.
352
+
353
+ This dependency provides access to the Worker instance that FastMCP
354
+ automatically creates for background task processing.
355
+
356
+ Returns:
357
+ A dependency that resolves to the active Worker instance
358
+
359
+ Raises:
360
+ RuntimeError: If not within a FastMCP server context
361
+
362
+ Example:
363
+ ```python
364
+ from fastmcp.dependencies import CurrentWorker
365
+
366
+ @mcp.tool()
367
+ async def check_worker_status(worker: Worker = CurrentWorker()) -> str:
368
+ return f"Worker: {worker.name}"
369
+ ```
370
+ """
371
+ return cast("Worker", _CurrentWorker())
372
+
373
+
374
+ class InMemoryProgress(DocketProgress):
375
+ """In-memory progress tracker for immediate tool execution.
376
+
377
+ Provides the same interface as Progress but stores state in memory
378
+ instead of Redis. Useful for testing and immediate execution where
379
+ progress doesn't need to be observable across processes.
380
+ """
381
+
382
+ def __init__(self) -> None:
383
+ super().__init__()
384
+ self._current: int | None = None
385
+ self._total: int = 1
386
+ self._message: str | None = None
387
+
388
+ async def __aenter__(self) -> DocketProgress:
389
+ return self
390
+
391
+ @property
392
+ def current(self) -> int | None:
393
+ return self._current
394
+
395
+ @property
396
+ def total(self) -> int:
397
+ return self._total
398
+
399
+ @property
400
+ def message(self) -> str | None:
401
+ return self._message
402
+
403
+ async def set_total(self, total: int) -> None:
404
+ """Set the total/target value for progress tracking."""
405
+ if total < 1:
406
+ raise ValueError("Total must be at least 1")
407
+ self._total = total
408
+
409
+ async def increment(self, amount: int = 1) -> None:
410
+ """Atomically increment the current progress value."""
411
+ if amount < 1:
412
+ raise ValueError("Amount must be at least 1")
413
+ if self._current is None:
414
+ self._current = amount
415
+ else:
416
+ self._current += amount
417
+
418
+ async def set_message(self, message: str | None) -> None:
419
+ """Update the progress status message."""
420
+ self._message = message
421
+
422
+
423
+ class Progress(DocketProgress):
424
+ """FastMCP Progress dependency that works in both server and worker contexts.
425
+
426
+ Extends Docket's Progress to handle two execution modes:
427
+ - In Docket worker: Uses the execution's progress (standard Docket behavior)
428
+ - In FastMCP server: Uses in-memory progress (not observable remotely)
429
+
430
+ This allows tools to use Progress() regardless of whether they're called
431
+ immediately or as background tasks.
432
+ """
433
+
434
+ async def __aenter__(self) -> DocketProgress:
435
+ # Try to get execution from Docket worker context
436
+ try:
437
+ return await super().__aenter__()
438
+ except LookupError:
439
+ # Not in worker context - return in-memory progress
440
+ docket = _current_docket.get()
441
+ if docket is None:
442
+ raise RuntimeError(
443
+ "Progress dependency requires a FastMCP server context."
444
+ ) from None
445
+
446
+ # Return in-memory progress for immediate execution
447
+ return InMemoryProgress()
448
+
449
+
450
+ class _CurrentFastMCP(Dependency):
451
+ """Internal dependency class for CurrentFastMCP."""
452
+
453
+ async def __aenter__(self):
454
+ server_ref = _current_server.get()
455
+ if server_ref is None:
456
+ raise RuntimeError("No FastMCP server instance in context")
457
+ server = server_ref()
458
+ if server is None:
459
+ raise RuntimeError("FastMCP server instance is no longer available")
460
+ return server
461
+
462
+
463
+ def CurrentFastMCP():
464
+ """Get the current FastMCP server instance.
465
+
466
+ This dependency provides access to the active FastMCP server.
467
+
468
+ Returns:
469
+ A dependency that resolves to the active FastMCP server
470
+
471
+ Raises:
472
+ RuntimeError: If no server in context (during resolution)
473
+
474
+ Example:
475
+ ```python
476
+ from fastmcp.dependencies import CurrentFastMCP
477
+
478
+ @mcp.tool()
479
+ async def introspect(server: FastMCP = CurrentFastMCP()) -> str:
480
+ return f"Server: {server.name}"
481
+ ```
482
+ """
483
+ from fastmcp.server.server import FastMCP
484
+
485
+ return cast(FastMCP, _CurrentFastMCP())
486
+
487
+
488
+ def get_server():
489
+ """Get the current FastMCP server instance directly.
490
+
491
+ Returns:
492
+ The active FastMCP server
493
+
494
+ Raises:
495
+ RuntimeError: If no server in context
496
+ """
497
+
498
+ server_ref = _current_server.get()
499
+ if server_ref is None:
500
+ raise RuntimeError("No FastMCP server instance in context")
501
+ server = server_ref()
502
+ if server is None:
503
+ raise RuntimeError("FastMCP server instance is no longer available")
504
+ return server
44
505
 
45
506
 
46
507
  def get_http_request() -> Request:
@@ -105,9 +566,6 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
105
566
  return {}
106
567
 
107
568
 
108
- # --- Access Token ---
109
-
110
-
111
569
  def get_access_token() -> AccessToken | None:
112
570
  """
113
571
  Get the FastMCP access token from the current context.