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
@@ -5,10 +5,10 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
- from typing import Any
9
- from urllib.parse import unquote
8
+ from typing import Annotated, Any
9
+ from urllib.parse import parse_qs, unquote
10
10
 
11
- from mcp.types import Annotations
11
+ from mcp.types import Annotations, Icon
12
12
  from mcp.types import ResourceTemplate as MCPResourceTemplate
13
13
  from pydantic import (
14
14
  Field,
@@ -17,17 +17,33 @@ from pydantic import (
17
17
  )
18
18
 
19
19
  from fastmcp.resources.resource import Resource
20
- from fastmcp.server.dependencies import get_context
20
+ from fastmcp.server.dependencies import get_context, without_injected_parameters
21
+ from fastmcp.server.tasks.config import TaskConfig
21
22
  from fastmcp.utilities.components import FastMCPComponent
22
23
  from fastmcp.utilities.json_schema import compress_schema
23
- from fastmcp.utilities.types import (
24
- find_kwarg_by_type,
25
- get_cached_typeadapter,
26
- )
24
+ from fastmcp.utilities.types import get_cached_typeadapter
25
+
26
+
27
+ def extract_query_params(uri_template: str) -> set[str]:
28
+ """Extract query parameter names from RFC 6570 `{?param1,param2}` syntax."""
29
+ match = re.search(r"\{\?([^}]+)\}", uri_template)
30
+ if match:
31
+ return {p.strip() for p in match.group(1).split(",")}
32
+ return set()
27
33
 
28
34
 
29
35
  def build_regex(template: str) -> re.Pattern:
30
- parts = re.split(r"(\{[^}]+\})", template)
36
+ """Build regex pattern for URI template, handling RFC 6570 syntax.
37
+
38
+ Supports:
39
+ - `{var}` - simple path parameter
40
+ - `{var*}` - wildcard path parameter (captures multiple segments)
41
+ - `{?var1,var2}` - query parameters (ignored in path matching)
42
+ """
43
+ # Remove query parameter syntax for path matching
44
+ template_without_query = re.sub(r"\{\?[^}]+\}", "", template)
45
+
46
+ parts = re.split(r"(\{[^}]+\})", template_without_query)
31
47
  pattern = ""
32
48
  for part in parts:
33
49
  if part.startswith("{") and part.endswith("}"):
@@ -43,11 +59,34 @@ def build_regex(template: str) -> re.Pattern:
43
59
 
44
60
 
45
61
  def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
62
+ """Match URI against template and extract both path and query parameters.
63
+
64
+ Supports RFC 6570 URI templates:
65
+ - Path params: `{var}`, `{var*}`
66
+ - Query params: `{?var1,var2}`
67
+ """
68
+ # Split URI into path and query parts
69
+ uri_path, _, query_string = uri.partition("?")
70
+
71
+ # Match path parameters
46
72
  regex = build_regex(uri_template)
47
- match = regex.match(uri)
48
- if match:
49
- return {k: unquote(v) for k, v in match.groupdict().items()}
50
- return None
73
+ match = regex.match(uri_path)
74
+ if not match:
75
+ return None
76
+
77
+ params = {k: unquote(v) for k, v in match.groupdict().items()}
78
+
79
+ # Extract query parameters if present in URI and template
80
+ if query_string:
81
+ query_param_names = extract_query_params(uri_template)
82
+ parsed_query = parse_qs(query_string)
83
+
84
+ for name in query_param_names:
85
+ if name in parsed_query:
86
+ # Take first value if multiple provided
87
+ params[name] = parsed_query[name][0] # type: ignore[index]
88
+
89
+ return params
51
90
 
52
91
 
53
92
  class ResourceTemplate(FastMCPComponent):
@@ -92,11 +131,13 @@ class ResourceTemplate(FastMCPComponent):
92
131
  name: str | None = None,
93
132
  title: str | None = None,
94
133
  description: str | None = None,
134
+ icons: list[Icon] | None = None,
95
135
  mime_type: str | None = None,
96
136
  tags: set[str] | None = None,
97
137
  enabled: bool | None = None,
98
138
  annotations: Annotations | None = None,
99
139
  meta: dict[str, Any] | None = None,
140
+ task: bool | TaskConfig | None = None,
100
141
  ) -> FunctionResourceTemplate:
101
142
  return FunctionResourceTemplate.from_function(
102
143
  fn=fn,
@@ -104,11 +145,13 @@ class ResourceTemplate(FastMCPComponent):
104
145
  name=name,
105
146
  title=title,
106
147
  description=description,
148
+ icons=icons,
107
149
  mime_type=mime_type,
108
150
  tags=tags,
109
151
  enabled=enabled,
110
152
  annotations=annotations,
111
153
  meta=meta,
154
+ task=task,
112
155
  )
113
156
 
114
157
  @field_validator("mime_type", mode="before")
@@ -130,21 +173,14 @@ class ResourceTemplate(FastMCPComponent):
130
173
  )
131
174
 
132
175
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
133
- """Create a resource from the template with the given parameters."""
134
-
135
- async def resource_read_fn() -> str | bytes:
136
- # Call function and check if result is a coroutine
137
- result = await self.read(arguments=params)
138
- return result
176
+ """Create a resource from the template with the given parameters.
139
177
 
140
- return Resource.from_function(
141
- fn=resource_read_fn,
142
- uri=uri,
143
- name=self.name,
144
- description=self.description,
145
- mime_type=self.mime_type,
146
- tags=self.tags,
147
- enabled=self.enabled,
178
+ The base implementation does not support background tasks.
179
+ Use FunctionResourceTemplate for task support.
180
+ """
181
+ raise NotImplementedError(
182
+ "Subclasses must implement create_resource(). "
183
+ "Use FunctionResourceTemplate for task support."
148
184
  )
149
185
 
150
186
  def to_mcp_template(
@@ -161,6 +197,7 @@ class ResourceTemplate(FastMCPComponent):
161
197
  description=overrides.get("description", self.description),
162
198
  mimeType=overrides.get("mimeType", self.mime_type),
163
199
  title=overrides.get("title", self.title),
200
+ icons=overrides.get("icons", self.icons),
164
201
  annotations=overrides.get("annotations", self.annotations),
165
202
  _meta=overrides.get(
166
203
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -195,20 +232,59 @@ class FunctionResourceTemplate(ResourceTemplate):
195
232
  """A template for dynamically creating resources."""
196
233
 
197
234
  fn: Callable[..., Any]
235
+ task_config: Annotated[
236
+ TaskConfig,
237
+ Field(description="Background task execution configuration (SEP-1686)."),
238
+ ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
239
+
240
+ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
241
+ """Create a resource from the template with the given parameters."""
242
+
243
+ async def resource_read_fn() -> str | bytes:
244
+ # Call function and check if result is a coroutine
245
+ result = await self.read(arguments=params)
246
+ return result
247
+
248
+ return Resource.from_function(
249
+ fn=resource_read_fn,
250
+ uri=uri,
251
+ name=self.name,
252
+ description=self.description,
253
+ mime_type=self.mime_type,
254
+ tags=self.tags,
255
+ enabled=self.enabled,
256
+ task=self.task_config,
257
+ )
198
258
 
199
259
  async def read(self, arguments: dict[str, Any]) -> str | bytes:
200
260
  """Read the resource content."""
201
- from fastmcp.server.context import Context
202
-
203
- # Add context to parameters if needed
261
+ # Type coercion for query parameters (which arrive as strings)
204
262
  kwargs = arguments.copy()
205
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
206
- if context_kwarg and context_kwarg not in kwargs:
207
- kwargs[context_kwarg] = get_context()
208
-
263
+ sig = inspect.signature(self.fn)
264
+ for param_name, param_value in list(kwargs.items()):
265
+ if param_name in sig.parameters and isinstance(param_value, str):
266
+ param = sig.parameters[param_name]
267
+ annotation = param.annotation
268
+
269
+ if annotation is inspect.Parameter.empty or annotation is str:
270
+ continue
271
+
272
+ try:
273
+ if annotation is int:
274
+ kwargs[param_name] = int(param_value)
275
+ elif annotation is float:
276
+ kwargs[param_name] = float(param_value)
277
+ elif annotation is bool:
278
+ kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
279
+ except (ValueError, AttributeError):
280
+ pass
281
+
282
+ # self.fn is wrapped by without_injected_parameters which handles
283
+ # dependency resolution internally, so we call it directly
209
284
  result = self.fn(**kwargs)
210
285
  if inspect.isawaitable(result):
211
286
  result = await result
287
+
212
288
  return result
213
289
 
214
290
  @classmethod
@@ -219,14 +295,15 @@ class FunctionResourceTemplate(ResourceTemplate):
219
295
  name: str | None = None,
220
296
  title: str | None = None,
221
297
  description: str | None = None,
298
+ icons: list[Icon] | None = None,
222
299
  mime_type: str | None = None,
223
300
  tags: set[str] | None = None,
224
301
  enabled: bool | None = None,
225
302
  annotations: Annotations | None = None,
226
303
  meta: dict[str, Any] | None = None,
304
+ task: bool | TaskConfig | None = None,
227
305
  ) -> FunctionResourceTemplate:
228
306
  """Create a template from a function."""
229
- from fastmcp.server.context import Context
230
307
 
231
308
  func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
232
309
  if func_name == "<lambda>":
@@ -241,46 +318,69 @@ class FunctionResourceTemplate(ResourceTemplate):
241
318
  "Functions with *args are not supported as resource templates"
242
319
  )
243
320
 
244
- # Auto-detect context parameter if not provided
245
-
246
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
321
+ # Extract path and query parameters from URI template
322
+ path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
323
+ query_params = extract_query_params(uri_template)
324
+ all_uri_params = path_params | query_params
247
325
 
248
- # Validate that URI params match function params
249
- uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
250
- if not uri_params:
326
+ if not all_uri_params:
251
327
  raise ValueError("URI template must contain at least one parameter")
252
328
 
253
- func_params = set(sig.parameters.keys())
254
- if context_kwarg:
255
- func_params.discard(context_kwarg)
329
+ # Use wrapper to get user-facing parameters (excludes injected params)
330
+ wrapper_fn = without_injected_parameters(fn)
331
+ user_sig = inspect.signature(wrapper_fn)
332
+ func_params = set(user_sig.parameters.keys())
256
333
 
257
- # get the parameters that are required
334
+ # Get required and optional function parameters
258
335
  required_params = {
259
336
  p
260
337
  for p in func_params
261
- if sig.parameters[p].default is inspect.Parameter.empty
262
- and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
263
- and p != context_kwarg
338
+ if user_sig.parameters[p].default is inspect.Parameter.empty
339
+ and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
264
340
  }
341
+ optional_params = {
342
+ p
343
+ for p in func_params
344
+ if user_sig.parameters[p].default is not inspect.Parameter.empty
345
+ and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
346
+ }
347
+
348
+ # Validate RFC 6570 query parameters
349
+ # Query params must be optional (have defaults)
350
+ if query_params:
351
+ invalid_query_params = query_params - optional_params
352
+ if invalid_query_params:
353
+ raise ValueError(
354
+ f"Query parameters {invalid_query_params} must be optional function parameters with default values"
355
+ )
265
356
 
266
- # Check if required parameters are a subset of the URI parameters
267
- if not required_params.issubset(uri_params):
357
+ # Check if required parameters are a subset of the path parameters
358
+ if not required_params.issubset(path_params):
268
359
  raise ValueError(
269
- f"Required function arguments {required_params} must be a subset of the URI parameters {uri_params}"
360
+ f"Required function arguments {required_params} must be a subset of the URI path parameters {path_params}"
270
361
  )
271
362
 
272
- # Check if the URI parameters are a subset of the function parameters (skip if **kwargs present)
363
+ # Check if all URI parameters are valid function parameters (skip if **kwargs present)
273
364
  if not any(
274
365
  param.kind == inspect.Parameter.VAR_KEYWORD
275
366
  for param in sig.parameters.values()
276
367
  ):
277
- if not uri_params.issubset(func_params):
368
+ if not all_uri_params.issubset(func_params):
278
369
  raise ValueError(
279
- f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
370
+ f"URI parameters {all_uri_params} must be a subset of the function arguments: {func_params}"
280
371
  )
281
372
 
282
373
  description = description or inspect.getdoc(fn)
283
374
 
375
+ # Normalize task to TaskConfig and validate
376
+ if task is None:
377
+ task_config = TaskConfig(mode="forbidden")
378
+ elif isinstance(task, bool):
379
+ task_config = TaskConfig.from_bool(task)
380
+ else:
381
+ task_config = task
382
+ task_config.validate_function(fn, func_name)
383
+
284
384
  # if the fn is a callable class, we need to get the __call__ method from here out
285
385
  if not inspect.isroutine(fn):
286
386
  fn = fn.__call__
@@ -288,21 +388,20 @@ class FunctionResourceTemplate(ResourceTemplate):
288
388
  if isinstance(fn, staticmethod):
289
389
  fn = fn.__func__
290
390
 
291
- type_adapter = get_cached_typeadapter(fn)
391
+ wrapper_fn = without_injected_parameters(fn)
392
+ type_adapter = get_cached_typeadapter(wrapper_fn)
292
393
  parameters = type_adapter.json_schema()
394
+ parameters = compress_schema(parameters, prune_titles=True)
293
395
 
294
- # compress the schema
295
- prune_params = [context_kwarg] if context_kwarg else None
296
- parameters = compress_schema(parameters, prune_params=prune_params)
297
-
298
- # ensure the arguments are properly cast
299
- fn = validate_call(fn)
396
+ # Use validate_call on wrapper for runtime type coercion
397
+ fn = validate_call(wrapper_fn)
300
398
 
301
399
  return cls(
302
400
  uri_template=uri_template,
303
401
  name=func_name,
304
402
  title=title,
305
403
  description=description,
404
+ icons=icons,
306
405
  mime_type=mime_type or "text/plain",
307
406
  fn=fn,
308
407
  parameters=parameters,
@@ -310,4 +409,5 @@ class FunctionResourceTemplate(ResourceTemplate):
310
409
  enabled=enabled if enabled is not None else True,
311
410
  annotations=annotations,
312
411
  meta=meta,
412
+ task_config=task_config,
313
413
  )
@@ -5,11 +5,11 @@ from __future__ import annotations
5
5
  import json
6
6
  from pathlib import Path
7
7
 
8
- import anyio
9
- import anyio.to_thread
10
8
  import httpx
11
9
  import pydantic.json
10
+ from anyio import Path as AsyncPath
12
11
  from pydantic import Field, ValidationInfo
12
+ from typing_extensions import override
13
13
 
14
14
  from fastmcp.exceptions import ResourceError
15
15
  from fastmcp.resources.resource import Resource
@@ -54,6 +54,10 @@ class FileResource(Resource):
54
54
  description="MIME type of the resource content",
55
55
  )
56
56
 
57
+ @property
58
+ def _async_path(self) -> AsyncPath:
59
+ return AsyncPath(self.path)
60
+
57
61
  @pydantic.field_validator("path")
58
62
  @classmethod
59
63
  def validate_absolute_path(cls, path: Path) -> Path:
@@ -71,12 +75,13 @@ class FileResource(Resource):
71
75
  mime_type = info.data.get("mime_type", "text/plain")
72
76
  return not mime_type.startswith("text/")
73
77
 
78
+ @override
74
79
  async def read(self) -> str | bytes:
75
80
  """Read the file content."""
76
81
  try:
77
82
  if self.is_binary:
78
- return await anyio.to_thread.run_sync(self.path.read_bytes)
79
- return await anyio.to_thread.run_sync(self.path.read_text)
83
+ return await self._async_path.read_bytes()
84
+ return await self._async_path.read_text()
80
85
  except Exception as e:
81
86
  raise ResourceError(f"Error reading file {self.path}") from e
82
87
 
@@ -89,11 +94,12 @@ class HttpResource(Resource):
89
94
  default="application/json", description="MIME type of the resource content"
90
95
  )
91
96
 
97
+ @override
92
98
  async def read(self) -> str | bytes:
93
99
  """Read the HTTP content."""
94
100
  async with httpx.AsyncClient() as client:
95
101
  response = await client.get(self.url)
96
- response.raise_for_status()
102
+ _ = response.raise_for_status()
97
103
  return response.text
98
104
 
99
105
 
@@ -111,6 +117,10 @@ class DirectoryResource(Resource):
111
117
  default="application/json", description="MIME type of the resource content"
112
118
  )
113
119
 
120
+ @property
121
+ def _async_path(self) -> AsyncPath:
122
+ return AsyncPath(self.path)
123
+
114
124
  @pydantic.field_validator("path")
115
125
  @classmethod
116
126
  def validate_absolute_path(cls, path: Path) -> Path:
@@ -119,33 +129,29 @@ class DirectoryResource(Resource):
119
129
  raise ValueError("Path must be absolute")
120
130
  return path
121
131
 
122
- def list_files(self) -> list[Path]:
132
+ async def list_files(self) -> list[Path]:
123
133
  """List files in the directory."""
124
- if not self.path.exists():
134
+ if not await self._async_path.exists():
125
135
  raise FileNotFoundError(f"Directory not found: {self.path}")
126
- if not self.path.is_dir():
136
+ if not await self._async_path.is_dir():
127
137
  raise NotADirectoryError(f"Not a directory: {self.path}")
128
138
 
139
+ pattern = self.pattern or "*"
140
+
141
+ glob_fn = self._async_path.rglob if self.recursive else self._async_path.glob
129
142
  try:
130
- if self.pattern:
131
- return (
132
- list(self.path.glob(self.pattern))
133
- if not self.recursive
134
- else list(self.path.rglob(self.pattern))
135
- )
136
- return (
137
- list(self.path.glob("*"))
138
- if not self.recursive
139
- else list(self.path.rglob("*"))
140
- )
143
+ return [Path(p) async for p in glob_fn(pattern) if await p.is_file()]
141
144
  except Exception as e:
142
- raise ResourceError(f"Error listing directory {self.path}: {e}")
145
+ raise ResourceError(f"Error listing directory {self.path}") from e
143
146
 
147
+ @override
144
148
  async def read(self) -> str: # Always returns JSON string
145
149
  """Read the directory listing."""
146
150
  try:
147
- files = await anyio.to_thread.run_sync(self.list_files)
148
- file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
151
+ files: list[Path] = await self.list_files()
152
+
153
+ file_list = [str(f.relative_to(self.path)) for f in files]
154
+
149
155
  return json.dumps({"files": file_list}, indent=2)
150
- except Exception:
151
- raise ResourceError(f"Error reading directory {self.path}")
156
+ except Exception as e:
157
+ raise ResourceError(f"Error reading directory {self.path}") from e
@@ -3,4 +3,4 @@ from .context import Context
3
3
  from . import dependencies
4
4
 
5
5
 
6
- __all__ = ["FastMCP", "Context"]
6
+ __all__ = ["Context", "FastMCP"]
@@ -5,26 +5,21 @@ from .auth import (
5
5
  AccessToken,
6
6
  AuthProvider,
7
7
  )
8
+ from .providers.debug import DebugTokenVerifier
8
9
  from .providers.jwt import JWTVerifier, StaticTokenVerifier
9
10
  from .oauth_proxy import OAuthProxy
11
+ from .oidc_proxy import OIDCProxy
10
12
 
11
13
 
12
14
  __all__ = [
15
+ "AccessToken",
13
16
  "AuthProvider",
14
- "OAuthProvider",
15
- "TokenVerifier",
17
+ "DebugTokenVerifier",
16
18
  "JWTVerifier",
17
- "StaticTokenVerifier",
18
- "RemoteAuthProvider",
19
- "AccessToken",
19
+ "OAuthProvider",
20
20
  "OAuthProxy",
21
+ "OIDCProxy",
22
+ "RemoteAuthProvider",
23
+ "StaticTokenVerifier",
24
+ "TokenVerifier",
21
25
  ]
22
-
23
-
24
- def __getattr__(name: str):
25
- # Defer import because it raises a deprecation warning
26
- if name == "BearerAuthProvider":
27
- from .providers.bearer import BearerAuthProvider
28
-
29
- return BearerAuthProvider
30
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")