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
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import warnings
7
7
  from collections.abc import Callable
8
- from typing import TYPE_CHECKING, Any
8
+ from typing import Any
9
9
 
10
10
  from pydantic import AnyUrl
11
11
 
@@ -19,9 +19,6 @@ from fastmcp.resources.template import (
19
19
  from fastmcp.settings import DuplicateBehavior
20
20
  from fastmcp.utilities.logging import get_logger
21
21
 
22
- if TYPE_CHECKING:
23
- from fastmcp.server.server import MountedServer
24
-
25
22
  logger = get_logger(__name__)
26
23
 
27
24
 
@@ -43,7 +40,6 @@ class ResourceManager:
43
40
  """
44
41
  self._resources: dict[str, Resource] = {}
45
42
  self._templates: dict[str, ResourceTemplate] = {}
46
- self._mounted_servers: list[MountedServer] = []
47
43
  self.mask_error_details = mask_error_details or settings.mask_error_details
48
44
 
49
45
  # Default to "warn" if None is provided
@@ -57,137 +53,13 @@ class ResourceManager:
57
53
  )
58
54
  self.duplicate_behavior = duplicate_behavior
59
55
 
60
- def mount(self, server: MountedServer) -> None:
61
- """Adds a mounted server as a source for resources and templates."""
62
- self._mounted_servers.append(server)
63
-
64
56
  async def get_resources(self) -> dict[str, Resource]:
65
57
  """Get all registered resources, keyed by URI."""
66
- return await self._load_resources(via_server=False)
58
+ return dict(self._resources)
67
59
 
68
60
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
69
61
  """Get all registered templates, keyed by URI template."""
70
- return await self._load_resource_templates(via_server=False)
71
-
72
- async def _load_resources(self, *, via_server: bool = False) -> dict[str, Resource]:
73
- """
74
- The single, consolidated recursive method for fetching resources. The 'via_server'
75
- parameter determines the communication path.
76
-
77
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
78
- - via_server=True: Server-to-server path for filtered MCP requests
79
- """
80
- all_resources: dict[str, Resource] = {}
81
-
82
- for mounted in self._mounted_servers:
83
- try:
84
- if via_server:
85
- # Use the server-to-server filtered path
86
- child_resources_list = await mounted.server._list_resources()
87
- child_resources = {
88
- resource.key: resource for resource in child_resources_list
89
- }
90
- else:
91
- # Use the manager-to-manager unfiltered path
92
- child_resources = (
93
- await mounted.server._resource_manager.get_resources()
94
- )
95
-
96
- # Apply prefix if needed
97
- if mounted.prefix:
98
- from fastmcp.server.server import add_resource_prefix
99
-
100
- for uri, resource in child_resources.items():
101
- prefixed_uri = add_resource_prefix(
102
- uri, mounted.prefix, mounted.resource_prefix_format
103
- )
104
- # Create a copy of the resource with the prefixed key and name
105
- prefixed_resource = resource.model_copy(
106
- update={"name": f"{mounted.prefix}_{resource.name}"},
107
- key=prefixed_uri,
108
- )
109
- all_resources[prefixed_uri] = prefixed_resource
110
- else:
111
- all_resources.update(child_resources)
112
- except Exception as e:
113
- # Skip failed mounts silently, matches existing behavior
114
- logger.warning(
115
- f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
116
- )
117
- if settings.mounted_components_raise_on_load_error:
118
- raise
119
- continue
120
-
121
- # Finally, add local resources, which always take precedence
122
- all_resources.update(self._resources)
123
- return all_resources
124
-
125
- async def _load_resource_templates(
126
- self, *, via_server: bool = False
127
- ) -> dict[str, ResourceTemplate]:
128
- """
129
- The single, consolidated recursive method for fetching templates. The 'via_server'
130
- parameter determines the communication path.
131
-
132
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
133
- - via_server=True: Server-to-server path for filtered MCP requests
134
- """
135
- all_templates: dict[str, ResourceTemplate] = {}
136
-
137
- for mounted in self._mounted_servers:
138
- try:
139
- if via_server:
140
- # Use the server-to-server filtered path
141
- child_templates = await mounted.server._list_resource_templates()
142
- else:
143
- # Use the manager-to-manager unfiltered path
144
- child_templates = (
145
- await mounted.server._resource_manager.list_resource_templates()
146
- )
147
- child_dict = {template.key: template for template in child_templates}
148
-
149
- # Apply prefix if needed
150
- if mounted.prefix:
151
- from fastmcp.server.server import add_resource_prefix
152
-
153
- for uri_template, template in child_dict.items():
154
- prefixed_uri_template = add_resource_prefix(
155
- uri_template, mounted.prefix, mounted.resource_prefix_format
156
- )
157
- # Create a copy of the template with the prefixed key and name
158
- prefixed_template = template.model_copy(
159
- update={"name": f"{mounted.prefix}_{template.name}"},
160
- key=prefixed_uri_template,
161
- )
162
- all_templates[prefixed_uri_template] = prefixed_template
163
- else:
164
- all_templates.update(child_dict)
165
- except Exception as e:
166
- # Skip failed mounts silently, matches existing behavior
167
- logger.warning(
168
- f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
169
- )
170
- if settings.mounted_components_raise_on_load_error:
171
- raise
172
- continue
173
-
174
- # Finally, add local templates, which always take precedence
175
- all_templates.update(self._templates)
176
- return all_templates
177
-
178
- async def list_resources(self) -> list[Resource]:
179
- """
180
- Lists all resources, applying protocol filtering.
181
- """
182
- resources_dict = await self._load_resources(via_server=True)
183
- return list(resources_dict.values())
184
-
185
- async def list_resource_templates(self) -> list[ResourceTemplate]:
186
- """
187
- Lists all templates, applying protocol filtering.
188
- """
189
- templates_dict = await self._load_resource_templates(via_server=True)
190
- return list(templates_dict.values())
62
+ return dict(self._templates)
191
63
 
192
64
  def add_resource_or_template_from_fn(
193
65
  self,
@@ -363,8 +235,8 @@ class ResourceManager:
363
235
 
364
236
  # Then check templates (local and mounted) only if not found in concrete resources
365
237
  templates = await self.get_resource_templates()
366
- for template_key in templates.keys():
367
- if match_uri_template(uri_str, template_key):
238
+ for template_key in templates:
239
+ if match_uri_template(uri_str, template_key) is not None:
368
240
  return True
369
241
 
370
242
  return False
@@ -381,16 +253,16 @@ class ResourceManager:
381
253
  uri_str = str(uri)
382
254
  logger.debug("Getting resource", extra={"uri": uri_str})
383
255
 
384
- # First check concrete resources (local and mounted)
256
+ # First check concrete resources
385
257
  resources = await self.get_resources()
386
258
  if resource := resources.get(uri_str):
387
259
  return resource
388
260
 
389
- # Then check templates (local and mounted) - use the utility function to match against storage keys
261
+ # Then check templates
390
262
  templates = await self.get_resource_templates()
391
263
  for storage_key, template in templates.items():
392
264
  # Try to match against the storage key (which might be a custom key)
393
- if params := match_uri_template(uri_str, storage_key):
265
+ if (params := match_uri_template(uri_str, storage_key)) is not None:
394
266
  try:
395
267
  return await template.create_resource(
396
268
  uri_str,
@@ -424,9 +296,6 @@ class ResourceManager:
424
296
  # 1. Check local resources first. The server will have already applied its filter.
425
297
  if uri_str in self._resources:
426
298
  resource = await self.get_resource(uri_str)
427
- if not resource:
428
- raise NotFoundError(f"Resource {uri_str!r} not found")
429
-
430
299
  try:
431
300
  return await resource.read()
432
301
 
@@ -449,7 +318,7 @@ class ResourceManager:
449
318
 
450
319
  # 1b. Check local templates if not found in concrete resources
451
320
  for key, template in self._templates.items():
452
- if params := match_uri_template(uri_str, key):
321
+ if (params := match_uri_template(uri_str, key)) is not None:
453
322
  try:
454
323
  resource = await template.create_resource(uri_str, params=params)
455
324
  return await resource.read()
@@ -471,32 +340,4 @@ class ResourceManager:
471
340
  f"Error reading resource from template {uri_str!r}: {e}"
472
341
  ) from e
473
342
 
474
- # 2. Check mounted servers using the filtered protocol path.
475
- from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
476
-
477
- for mounted in reversed(self._mounted_servers):
478
- key = uri_str
479
- try:
480
- if mounted.prefix:
481
- if has_resource_prefix(
482
- key,
483
- mounted.prefix,
484
- mounted.resource_prefix_format,
485
- ):
486
- key = remove_resource_prefix(
487
- key,
488
- mounted.prefix,
489
- mounted.resource_prefix_format,
490
- )
491
- else:
492
- continue
493
-
494
- try:
495
- result = await mounted.server._read_resource(key)
496
- return result[0].content
497
- except NotFoundError:
498
- continue
499
- except NotFoundError:
500
- continue
501
-
502
343
  raise NotFoundError(f"Resource {uri_str!r} not found.")
@@ -6,9 +6,9 @@ import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
8
  from typing import Any
9
- from urllib.parse import unquote
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,
@@ -26,8 +26,26 @@ from fastmcp.utilities.types import (
26
26
  )
27
27
 
28
28
 
29
+ def extract_query_params(uri_template: str) -> set[str]:
30
+ """Extract query parameter names from RFC 6570 `{?param1,param2}` syntax."""
31
+ match = re.search(r"\{\?([^}]+)\}", uri_template)
32
+ if match:
33
+ return {p.strip() for p in match.group(1).split(",")}
34
+ return set()
35
+
36
+
29
37
  def build_regex(template: str) -> re.Pattern:
30
- parts = re.split(r"(\{[^}]+\})", template)
38
+ """Build regex pattern for URI template, handling RFC 6570 syntax.
39
+
40
+ Supports:
41
+ - `{var}` - simple path parameter
42
+ - `{var*}` - wildcard path parameter (captures multiple segments)
43
+ - `{?var1,var2}` - query parameters (ignored in path matching)
44
+ """
45
+ # Remove query parameter syntax for path matching
46
+ template_without_query = re.sub(r"\{\?[^}]+\}", "", template)
47
+
48
+ parts = re.split(r"(\{[^}]+\})", template_without_query)
31
49
  pattern = ""
32
50
  for part in parts:
33
51
  if part.startswith("{") and part.endswith("}"):
@@ -43,11 +61,34 @@ def build_regex(template: str) -> re.Pattern:
43
61
 
44
62
 
45
63
  def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
64
+ """Match URI against template and extract both path and query parameters.
65
+
66
+ Supports RFC 6570 URI templates:
67
+ - Path params: `{var}`, `{var*}`
68
+ - Query params: `{?var1,var2}`
69
+ """
70
+ # Split URI into path and query parts
71
+ uri_path, _, query_string = uri.partition("?")
72
+
73
+ # Match path parameters
46
74
  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
75
+ match = regex.match(uri_path)
76
+ if not match:
77
+ return None
78
+
79
+ params = {k: unquote(v) for k, v in match.groupdict().items()}
80
+
81
+ # Extract query parameters if present in URI and template
82
+ if query_string:
83
+ query_param_names = extract_query_params(uri_template)
84
+ parsed_query = parse_qs(query_string)
85
+
86
+ for name in query_param_names:
87
+ if name in parsed_query:
88
+ # Take first value if multiple provided
89
+ params[name] = parsed_query[name][0] # type: ignore[index]
90
+
91
+ return params
51
92
 
52
93
 
53
94
  class ResourceTemplate(FastMCPComponent):
@@ -92,6 +133,7 @@ class ResourceTemplate(FastMCPComponent):
92
133
  name: str | None = None,
93
134
  title: str | None = None,
94
135
  description: str | None = None,
136
+ icons: list[Icon] | None = None,
95
137
  mime_type: str | None = None,
96
138
  tags: set[str] | None = None,
97
139
  enabled: bool | None = None,
@@ -104,6 +146,7 @@ class ResourceTemplate(FastMCPComponent):
104
146
  name=name,
105
147
  title=title,
106
148
  description=description,
149
+ icons=icons,
107
150
  mime_type=mime_type,
108
151
  tags=tags,
109
152
  enabled=enabled,
@@ -154,16 +197,19 @@ class ResourceTemplate(FastMCPComponent):
154
197
  **overrides: Any,
155
198
  ) -> MCPResourceTemplate:
156
199
  """Convert the resource template to an MCPResourceTemplate."""
157
- kwargs = {
158
- "uriTemplate": self.uri_template,
159
- "name": self.name,
160
- "description": self.description,
161
- "mimeType": self.mime_type,
162
- "title": self.title,
163
- "annotations": self.annotations,
164
- "_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
165
- }
166
- return MCPResourceTemplate(**kwargs | overrides)
200
+
201
+ return MCPResourceTemplate(
202
+ name=overrides.get("name", self.name),
203
+ uriTemplate=overrides.get("uriTemplate", self.uri_template),
204
+ description=overrides.get("description", self.description),
205
+ mimeType=overrides.get("mimeType", self.mime_type),
206
+ title=overrides.get("title", self.title),
207
+ icons=overrides.get("icons", self.icons),
208
+ annotations=overrides.get("annotations", self.annotations),
209
+ _meta=overrides.get(
210
+ "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
211
+ ),
212
+ )
167
213
 
168
214
  @classmethod
169
215
  def from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate:
@@ -204,6 +250,31 @@ class FunctionResourceTemplate(ResourceTemplate):
204
250
  if context_kwarg and context_kwarg not in kwargs:
205
251
  kwargs[context_kwarg] = get_context()
206
252
 
253
+ # Type coercion for query parameters (which arrive as strings)
254
+ # Get function signature for type hints
255
+ sig = inspect.signature(self.fn)
256
+ for param_name, param_value in list(kwargs.items()):
257
+ if param_name in sig.parameters and isinstance(param_value, str):
258
+ param = sig.parameters[param_name]
259
+ annotation = param.annotation
260
+
261
+ # Skip if no annotation or annotation is str
262
+ if annotation is inspect.Parameter.empty or annotation is str:
263
+ continue
264
+
265
+ # Handle common type coercions
266
+ try:
267
+ if annotation is int:
268
+ kwargs[param_name] = int(param_value)
269
+ elif annotation is float:
270
+ kwargs[param_name] = float(param_value)
271
+ elif annotation is bool:
272
+ # Handle boolean strings
273
+ kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
274
+ except (ValueError, AttributeError):
275
+ # Let validate_call handle the error
276
+ pass
277
+
207
278
  result = self.fn(**kwargs)
208
279
  if inspect.isawaitable(result):
209
280
  result = await result
@@ -217,6 +288,7 @@ class FunctionResourceTemplate(ResourceTemplate):
217
288
  name: str | None = None,
218
289
  title: str | None = None,
219
290
  description: str | None = None,
291
+ icons: list[Icon] | None = None,
220
292
  mime_type: str | None = None,
221
293
  tags: set[str] | None = None,
222
294
  enabled: bool | None = None,
@@ -243,16 +315,19 @@ class FunctionResourceTemplate(ResourceTemplate):
243
315
 
244
316
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
245
317
 
246
- # Validate that URI params match function params
247
- uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
248
- if not uri_params:
318
+ # Extract path and query parameters from URI template
319
+ path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
320
+ query_params = extract_query_params(uri_template)
321
+ all_uri_params = path_params | query_params
322
+
323
+ if not all_uri_params:
249
324
  raise ValueError("URI template must contain at least one parameter")
250
325
 
251
326
  func_params = set(sig.parameters.keys())
252
327
  if context_kwarg:
253
328
  func_params.discard(context_kwarg)
254
329
 
255
- # get the parameters that are required
330
+ # Get required and optional function parameters
256
331
  required_params = {
257
332
  p
258
333
  for p in func_params
@@ -260,21 +335,37 @@ class FunctionResourceTemplate(ResourceTemplate):
260
335
  and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
261
336
  and p != context_kwarg
262
337
  }
338
+ optional_params = {
339
+ p
340
+ for p in func_params
341
+ if sig.parameters[p].default is not inspect.Parameter.empty
342
+ and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
343
+ and p != context_kwarg
344
+ }
345
+
346
+ # Validate RFC 6570 query parameters
347
+ # Query params must be optional (have defaults)
348
+ if query_params:
349
+ invalid_query_params = query_params - optional_params
350
+ if invalid_query_params:
351
+ raise ValueError(
352
+ f"Query parameters {invalid_query_params} must be optional function parameters with default values"
353
+ )
263
354
 
264
- # Check if required parameters are a subset of the URI parameters
265
- if not required_params.issubset(uri_params):
355
+ # Check if required parameters are a subset of the path parameters
356
+ if not required_params.issubset(path_params):
266
357
  raise ValueError(
267
- f"Required function arguments {required_params} must be a subset of the URI parameters {uri_params}"
358
+ f"Required function arguments {required_params} must be a subset of the URI path parameters {path_params}"
268
359
  )
269
360
 
270
- # Check if the URI parameters are a subset of the function parameters (skip if **kwargs present)
361
+ # Check if all URI parameters are valid function parameters (skip if **kwargs present)
271
362
  if not any(
272
363
  param.kind == inspect.Parameter.VAR_KEYWORD
273
364
  for param in sig.parameters.values()
274
365
  ):
275
- if not uri_params.issubset(func_params):
366
+ if not all_uri_params.issubset(func_params):
276
367
  raise ValueError(
277
- f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
368
+ f"URI parameters {all_uri_params} must be a subset of the function arguments: {func_params}"
278
369
  )
279
370
 
280
371
  description = description or inspect.getdoc(fn)
@@ -301,6 +392,7 @@ class FunctionResourceTemplate(ResourceTemplate):
301
392
  name=func_name,
302
393
  title=title,
303
394
  description=description,
395
+ icons=icons,
304
396
  mime_type=mime_type or "text/plain",
305
397
  fn=fn,
306
398
  parameters=parameters,
@@ -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,19 +5,23 @@ 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
26
 
23
27