fastmcp 2.12.4__py3-none-any.whl → 2.13.0rc1__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 (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +2 -2
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.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,
@@ -381,12 +253,12 @@ 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)
@@ -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
 
@@ -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,
@@ -161,6 +204,7 @@ class ResourceTemplate(FastMCPComponent):
161
204
  description=overrides.get("description", self.description),
162
205
  mimeType=overrides.get("mimeType", self.mime_type),
163
206
  title=overrides.get("title", self.title),
207
+ icons=overrides.get("icons", self.icons),
164
208
  annotations=overrides.get("annotations", self.annotations),
165
209
  _meta=overrides.get(
166
210
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -206,6 +250,31 @@ class FunctionResourceTemplate(ResourceTemplate):
206
250
  if context_kwarg and context_kwarg not in kwargs:
207
251
  kwargs[context_kwarg] = get_context()
208
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
+
209
278
  result = self.fn(**kwargs)
210
279
  if inspect.isawaitable(result):
211
280
  result = await result
@@ -219,6 +288,7 @@ class FunctionResourceTemplate(ResourceTemplate):
219
288
  name: str | None = None,
220
289
  title: str | None = None,
221
290
  description: str | None = None,
291
+ icons: list[Icon] | None = None,
222
292
  mime_type: str | None = None,
223
293
  tags: set[str] | None = None,
224
294
  enabled: bool | None = None,
@@ -245,16 +315,19 @@ class FunctionResourceTemplate(ResourceTemplate):
245
315
 
246
316
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
247
317
 
248
- # Validate that URI params match function params
249
- uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
250
- 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:
251
324
  raise ValueError("URI template must contain at least one parameter")
252
325
 
253
326
  func_params = set(sig.parameters.keys())
254
327
  if context_kwarg:
255
328
  func_params.discard(context_kwarg)
256
329
 
257
- # get the parameters that are required
330
+ # Get required and optional function parameters
258
331
  required_params = {
259
332
  p
260
333
  for p in func_params
@@ -262,21 +335,37 @@ class FunctionResourceTemplate(ResourceTemplate):
262
335
  and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
263
336
  and p != context_kwarg
264
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
+ )
265
354
 
266
- # Check if required parameters are a subset of the URI parameters
267
- 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):
268
357
  raise ValueError(
269
- 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}"
270
359
  )
271
360
 
272
- # 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)
273
362
  if not any(
274
363
  param.kind == inspect.Parameter.VAR_KEYWORD
275
364
  for param in sig.parameters.values()
276
365
  ):
277
- if not uri_params.issubset(func_params):
366
+ if not all_uri_params.issubset(func_params):
278
367
  raise ValueError(
279
- 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}"
280
369
  )
281
370
 
282
371
  description = description or inspect.getdoc(fn)
@@ -303,6 +392,7 @@ class FunctionResourceTemplate(ResourceTemplate):
303
392
  name=func_name,
304
393
  title=title,
305
394
  description=description,
395
+ icons=icons,
306
396
  mime_type=mime_type or "text/plain",
307
397
  fn=fn,
308
398
  parameters=parameters,
@@ -3,10 +3,7 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
6
- from mcp.server.auth.middleware.bearer_auth import (
7
- BearerAuthBackend,
8
- RequireAuthMiddleware,
9
- )
6
+ from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
10
7
  from mcp.server.auth.provider import (
11
8
  AccessToken as _SDKAccessToken,
12
9
  )
@@ -81,10 +78,10 @@ class AuthProvider(TokenVerifierProtocol):
81
78
  def get_routes(
82
79
  self,
83
80
  mcp_path: str | None = None,
84
- mcp_endpoint: Any | None = None,
85
81
  ) -> list[Route]:
86
- """Get the routes for this authentication provider.
82
+ """Get all routes for this authentication provider.
87
83
 
84
+ This includes both well-known discovery routes and operational routes.
88
85
  Each provider is responsible for creating whatever routes it needs:
89
86
  - TokenVerifier: typically no routes (default implementation)
90
87
  - RemoteAuthProvider: protected resource metadata routes
@@ -93,30 +90,45 @@ class AuthProvider(TokenVerifierProtocol):
93
90
 
94
91
  Args:
95
92
  mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
96
- mcp_endpoint: The MCP endpoint handler to protect with auth
93
+ This is used to advertise the resource URL in metadata, but the
94
+ provider does not create the actual MCP endpoint route.
97
95
 
98
96
  Returns:
99
- List of routes for this provider, including protected MCP endpoints if provided
97
+ List of all routes for this provider (excluding the MCP endpoint itself)
100
98
  """
99
+ return []
101
100
 
102
- routes = []
101
+ def get_well_known_routes(
102
+ self,
103
+ mcp_path: str | None = None,
104
+ ) -> list[Route]:
105
+ """Get well-known discovery routes for this authentication provider.
103
106
 
104
- # Add protected MCP endpoint if provided
105
- if mcp_path and mcp_endpoint:
106
- resource_metadata_url = self._get_resource_url(
107
- "/.well-known/oauth-protected-resource"
108
- )
107
+ This is a utility method that filters get_routes() to return only
108
+ well-known discovery routes (those starting with /.well-known/).
109
109
 
110
- routes.append(
111
- Route(
112
- mcp_path,
113
- endpoint=RequireAuthMiddleware(
114
- mcp_endpoint, self.required_scopes, resource_metadata_url
115
- ),
116
- )
117
- )
110
+ Well-known routes provide OAuth metadata and discovery endpoints that
111
+ clients use to discover authentication capabilities. These routes should
112
+ be mounted at the root level of the application to comply with RFC 8414
113
+ and RFC 9728.
118
114
 
119
- return routes
115
+ Common well-known routes:
116
+ - /.well-known/oauth-authorization-server (authorization server metadata)
117
+ - /.well-known/oauth-protected-resource/* (protected resource metadata)
118
+
119
+ Args:
120
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
121
+ This is used to construct path-scoped well-known URLs.
122
+
123
+ Returns:
124
+ List of well-known discovery routes (typically mounted at root level)
125
+ """
126
+ all_routes = self.get_routes(mcp_path)
127
+ return [
128
+ route
129
+ for route in all_routes
130
+ if isinstance(route, Route) and route.path.startswith("/.well-known/")
131
+ ]
120
132
 
121
133
  def get_middleware(self) -> list:
122
134
  """Get HTTP application-level middleware for this auth provider.
@@ -225,14 +237,12 @@ class RemoteAuthProvider(AuthProvider):
225
237
  def get_routes(
226
238
  self,
227
239
  mcp_path: str | None = None,
228
- mcp_endpoint: Any | None = None,
229
240
  ) -> list[Route]:
230
- """Get OAuth routes for this provider.
241
+ """Get routes for this provider.
231
242
 
232
- Creates protected resource metadata routes and optionally wraps MCP endpoints with auth.
243
+ Creates protected resource metadata routes (RFC 9728).
233
244
  """
234
- # Start with base routes (protected MCP endpoint)
235
- routes = super().get_routes(mcp_path, mcp_endpoint)
245
+ routes = []
236
246
 
237
247
  # Get the resource URL based on the MCP path
238
248
  resource_url = self._get_resource_url(mcp_path)
@@ -326,14 +336,12 @@ class OAuthProvider(
326
336
  def get_routes(
327
337
  self,
328
338
  mcp_path: str | None = None,
329
- mcp_endpoint: Any | None = None,
330
339
  ) -> list[Route]:
331
340
  """Get OAuth authorization server routes and optional protected resource routes.
332
341
 
333
342
  This method creates the full set of OAuth routes including:
334
343
  - Standard OAuth authorization server routes (/.well-known/oauth-authorization-server, /authorize, /token, etc.)
335
344
  - Optional protected resource routes
336
- - Protected MCP endpoints if provided
337
345
 
338
346
  Returns:
339
347
  List of OAuth routes
@@ -366,7 +374,7 @@ class OAuthProvider(
366
374
  )
367
375
  oauth_routes.extend(protected_routes)
368
376
 
369
- # Add protected MCP endpoint from base class
370
- oauth_routes.extend(super().get_routes(mcp_path, mcp_endpoint))
377
+ # Add base routes
378
+ oauth_routes.extend(super().get_routes(mcp_path))
371
379
 
372
380
  return oauth_routes