fastmcp 2.13.2__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.
- fastmcp/__init__.py +0 -21
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +8 -22
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/auth/oauth.py +9 -9
- fastmcp/client/client.py +665 -129
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/messages.py +7 -5
- fastmcp/client/roots.py +2 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +37 -5
- fastmcp/contrib/component_manager/component_service.py +4 -20
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +1 -1
- fastmcp/experimental/server/openapi/__init__.py +15 -13
- fastmcp/experimental/utilities/openapi/__init__.py +12 -38
- fastmcp/prompts/prompt.py +33 -33
- fastmcp/resources/resource.py +29 -12
- fastmcp/resources/template.py +64 -54
- fastmcp/server/auth/__init__.py +0 -9
- fastmcp/server/auth/auth.py +127 -3
- fastmcp/server/auth/oauth_proxy.py +47 -97
- fastmcp/server/auth/oidc_proxy.py +7 -0
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/oci.py +2 -2
- fastmcp/server/context.py +66 -72
- fastmcp/server/dependencies.py +464 -6
- fastmcp/server/elicitation.py +285 -47
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +15 -3
- fastmcp/server/low_level.py +56 -12
- fastmcp/server/middleware/middleware.py +2 -2
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +4 -3
- fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +50 -37
- fastmcp/server/server.py +731 -532
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +101 -103
- fastmcp/tools/tool.py +80 -44
- fastmcp/tools/tool_transform.py +1 -12
- fastmcp/utilities/components.py +3 -3
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
- fastmcp/utilities/tests.py +11 -5
- fastmcp/utilities/types.py +8 -0
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/METADATA +5 -4
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/RECORD +71 -59
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1087
- fastmcp/utilities/openapi.py +0 -1568
- /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/resources/template.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Annotated, Any
|
|
9
9
|
from urllib.parse import parse_qs, unquote
|
|
10
10
|
|
|
11
11
|
from mcp.types import Annotations, Icon
|
|
@@ -17,13 +17,11 @@ 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
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
def extract_query_params(uri_template: str) -> set[str]:
|
|
@@ -139,6 +137,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
139
137
|
enabled: bool | None = None,
|
|
140
138
|
annotations: Annotations | None = None,
|
|
141
139
|
meta: dict[str, Any] | None = None,
|
|
140
|
+
task: bool | TaskConfig | None = None,
|
|
142
141
|
) -> FunctionResourceTemplate:
|
|
143
142
|
return FunctionResourceTemplate.from_function(
|
|
144
143
|
fn=fn,
|
|
@@ -152,6 +151,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
152
151
|
enabled=enabled,
|
|
153
152
|
annotations=annotations,
|
|
154
153
|
meta=meta,
|
|
154
|
+
task=task,
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
@field_validator("mime_type", mode="before")
|
|
@@ -173,21 +173,14 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
173
173
|
)
|
|
174
174
|
|
|
175
175
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
176
|
-
"""Create a resource from the template with the given parameters.
|
|
177
|
-
|
|
178
|
-
async def resource_read_fn() -> str | bytes:
|
|
179
|
-
# Call function and check if result is a coroutine
|
|
180
|
-
result = await self.read(arguments=params)
|
|
181
|
-
return result
|
|
176
|
+
"""Create a resource from the template with the given parameters.
|
|
182
177
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
tags=self.tags,
|
|
190
|
-
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."
|
|
191
184
|
)
|
|
192
185
|
|
|
193
186
|
def to_mcp_template(
|
|
@@ -239,45 +232,59 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
239
232
|
"""A template for dynamically creating resources."""
|
|
240
233
|
|
|
241
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"))
|
|
242
239
|
|
|
243
|
-
async def
|
|
244
|
-
"""
|
|
245
|
-
from fastmcp.server.context import Context
|
|
240
|
+
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
241
|
+
"""Create a resource from the template with the given parameters."""
|
|
246
242
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
)
|
|
252
258
|
|
|
259
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
260
|
+
"""Read the resource content."""
|
|
253
261
|
# Type coercion for query parameters (which arrive as strings)
|
|
254
|
-
|
|
262
|
+
kwargs = arguments.copy()
|
|
255
263
|
sig = inspect.signature(self.fn)
|
|
256
264
|
for param_name, param_value in list(kwargs.items()):
|
|
257
265
|
if param_name in sig.parameters and isinstance(param_value, str):
|
|
258
266
|
param = sig.parameters[param_name]
|
|
259
267
|
annotation = param.annotation
|
|
260
268
|
|
|
261
|
-
# Skip if no annotation or annotation is str
|
|
262
269
|
if annotation is inspect.Parameter.empty or annotation is str:
|
|
263
270
|
continue
|
|
264
271
|
|
|
265
|
-
# Handle common type coercions
|
|
266
272
|
try:
|
|
267
273
|
if annotation is int:
|
|
268
274
|
kwargs[param_name] = int(param_value)
|
|
269
275
|
elif annotation is float:
|
|
270
276
|
kwargs[param_name] = float(param_value)
|
|
271
277
|
elif annotation is bool:
|
|
272
|
-
# Handle boolean strings
|
|
273
278
|
kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
|
|
274
279
|
except (ValueError, AttributeError):
|
|
275
|
-
# Let validate_call handle the error
|
|
276
280
|
pass
|
|
277
281
|
|
|
282
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
283
|
+
# dependency resolution internally, so we call it directly
|
|
278
284
|
result = self.fn(**kwargs)
|
|
279
285
|
if inspect.isawaitable(result):
|
|
280
286
|
result = await result
|
|
287
|
+
|
|
281
288
|
return result
|
|
282
289
|
|
|
283
290
|
@classmethod
|
|
@@ -294,9 +301,9 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
294
301
|
enabled: bool | None = None,
|
|
295
302
|
annotations: Annotations | None = None,
|
|
296
303
|
meta: dict[str, Any] | None = None,
|
|
304
|
+
task: bool | TaskConfig | None = None,
|
|
297
305
|
) -> FunctionResourceTemplate:
|
|
298
306
|
"""Create a template from a function."""
|
|
299
|
-
from fastmcp.server.context import Context
|
|
300
307
|
|
|
301
308
|
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
302
309
|
if func_name == "<lambda>":
|
|
@@ -311,10 +318,6 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
311
318
|
"Functions with *args are not supported as resource templates"
|
|
312
319
|
)
|
|
313
320
|
|
|
314
|
-
# Auto-detect context parameter if not provided
|
|
315
|
-
|
|
316
|
-
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
317
|
-
|
|
318
321
|
# Extract path and query parameters from URI template
|
|
319
322
|
path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
320
323
|
query_params = extract_query_params(uri_template)
|
|
@@ -323,24 +326,23 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
323
326
|
if not all_uri_params:
|
|
324
327
|
raise ValueError("URI template must contain at least one parameter")
|
|
325
328
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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())
|
|
329
333
|
|
|
330
334
|
# Get required and optional function parameters
|
|
331
335
|
required_params = {
|
|
332
336
|
p
|
|
333
337
|
for p in func_params
|
|
334
|
-
if
|
|
335
|
-
and
|
|
336
|
-
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
|
|
337
340
|
}
|
|
338
341
|
optional_params = {
|
|
339
342
|
p
|
|
340
343
|
for p in func_params
|
|
341
|
-
if
|
|
342
|
-
and
|
|
343
|
-
and p != context_kwarg
|
|
344
|
+
if user_sig.parameters[p].default is not inspect.Parameter.empty
|
|
345
|
+
and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
|
|
344
346
|
}
|
|
345
347
|
|
|
346
348
|
# Validate RFC 6570 query parameters
|
|
@@ -370,6 +372,15 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
370
372
|
|
|
371
373
|
description = description or inspect.getdoc(fn)
|
|
372
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
|
+
|
|
373
384
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
374
385
|
if not inspect.isroutine(fn):
|
|
375
386
|
fn = fn.__call__
|
|
@@ -377,15 +388,13 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
377
388
|
if isinstance(fn, staticmethod):
|
|
378
389
|
fn = fn.__func__
|
|
379
390
|
|
|
380
|
-
|
|
391
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
392
|
+
type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
381
393
|
parameters = type_adapter.json_schema()
|
|
394
|
+
parameters = compress_schema(parameters, prune_titles=True)
|
|
382
395
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
386
|
-
|
|
387
|
-
# ensure the arguments are properly cast
|
|
388
|
-
fn = validate_call(fn)
|
|
396
|
+
# Use validate_call on wrapper for runtime type coercion
|
|
397
|
+
fn = validate_call(wrapper_fn)
|
|
389
398
|
|
|
390
399
|
return cls(
|
|
391
400
|
uri_template=uri_template,
|
|
@@ -400,4 +409,5 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
400
409
|
enabled=enabled if enabled is not None else True,
|
|
401
410
|
annotations=annotations,
|
|
402
411
|
meta=meta,
|
|
412
|
+
task_config=task_config,
|
|
403
413
|
)
|
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -23,12 +23,3 @@ __all__ = [
|
|
|
23
23
|
"StaticTokenVerifier",
|
|
24
24
|
"TokenVerifier",
|
|
25
25
|
]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def __getattr__(name: str):
|
|
29
|
-
# Defer import because it raises a deprecation warning
|
|
30
|
-
if name == "BearerAuthProvider":
|
|
31
|
-
from .providers.bearer import BearerAuthProvider
|
|
32
|
-
|
|
33
|
-
return BearerAuthProvider
|
|
34
|
-
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
fastmcp/server/auth/auth.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from typing import Any, cast
|
|
5
|
+
from urllib.parse import urlparse
|
|
4
6
|
|
|
7
|
+
from mcp.server.auth.handlers.token import TokenErrorResponse
|
|
8
|
+
from mcp.server.auth.handlers.token import TokenHandler as _SDKTokenHandler
|
|
9
|
+
from mcp.server.auth.json_response import PydanticJSONResponse
|
|
5
10
|
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
|
6
11
|
from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
|
|
12
|
+
from mcp.server.auth.middleware.client_auth import ClientAuthenticator
|
|
7
13
|
from mcp.server.auth.provider import (
|
|
8
14
|
AccessToken as _SDKAccessToken,
|
|
9
15
|
)
|
|
@@ -16,6 +22,7 @@ from mcp.server.auth.provider import (
|
|
|
16
22
|
TokenVerifier as TokenVerifierProtocol,
|
|
17
23
|
)
|
|
18
24
|
from mcp.server.auth.routes import (
|
|
25
|
+
cors_middleware,
|
|
19
26
|
create_auth_routes,
|
|
20
27
|
create_protected_resource_routes,
|
|
21
28
|
)
|
|
@@ -39,6 +46,48 @@ class AccessToken(_SDKAccessToken):
|
|
|
39
46
|
claims: dict[str, Any] = Field(default_factory=dict)
|
|
40
47
|
|
|
41
48
|
|
|
49
|
+
class TokenHandler(_SDKTokenHandler):
|
|
50
|
+
"""TokenHandler that returns OAuth 2.1 compliant error responses.
|
|
51
|
+
|
|
52
|
+
The MCP SDK returns `unauthorized_client` for client authentication failures.
|
|
53
|
+
However, per RFC 6749 Section 5.2, authentication failures should return
|
|
54
|
+
`invalid_client` with HTTP 401, not `unauthorized_client`.
|
|
55
|
+
|
|
56
|
+
This distinction matters: `unauthorized_client` means "client exists but
|
|
57
|
+
can't do this", while `invalid_client` means "client doesn't exist or
|
|
58
|
+
credentials are wrong". Claude's OAuth client uses this to decide whether
|
|
59
|
+
to re-register.
|
|
60
|
+
|
|
61
|
+
This handler transforms 401 responses with `unauthorized_client` to use
|
|
62
|
+
`invalid_client` instead, making the error semantics correct per OAuth spec.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
async def handle(self, request: Any):
|
|
66
|
+
"""Wrap SDK handle() and transform auth error responses."""
|
|
67
|
+
response = await super().handle(request)
|
|
68
|
+
|
|
69
|
+
# Transform 401 unauthorized_client -> invalid_client
|
|
70
|
+
if response.status_code == 401:
|
|
71
|
+
try:
|
|
72
|
+
body = json.loads(response.body)
|
|
73
|
+
if body.get("error") == "unauthorized_client":
|
|
74
|
+
return PydanticJSONResponse(
|
|
75
|
+
content=TokenErrorResponse(
|
|
76
|
+
error="invalid_client",
|
|
77
|
+
error_description=body.get("error_description"),
|
|
78
|
+
),
|
|
79
|
+
status_code=401,
|
|
80
|
+
headers={
|
|
81
|
+
"Cache-Control": "no-store",
|
|
82
|
+
"Pragma": "no-cache",
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
except (json.JSONDecodeError, AttributeError):
|
|
86
|
+
pass # Not JSON or unexpected format, return as-is
|
|
87
|
+
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
|
|
42
91
|
class AuthProvider(TokenVerifierProtocol):
|
|
43
92
|
"""Base class for all FastMCP authentication providers.
|
|
44
93
|
|
|
@@ -140,12 +189,13 @@ class AuthProvider(TokenVerifierProtocol):
|
|
|
140
189
|
Returns:
|
|
141
190
|
List of Starlette Middleware instances to apply to the HTTP app
|
|
142
191
|
"""
|
|
192
|
+
# TODO(ty): remove type ignores when ty supports Starlette Middleware typing
|
|
143
193
|
return [
|
|
144
194
|
Middleware(
|
|
145
|
-
AuthenticationMiddleware,
|
|
195
|
+
AuthenticationMiddleware, # type: ignore[arg-type]
|
|
146
196
|
backend=BearerAuthBackend(self),
|
|
147
197
|
),
|
|
148
|
-
Middleware(AuthContextMiddleware),
|
|
198
|
+
Middleware(AuthContextMiddleware), # type: ignore[arg-type]
|
|
149
199
|
]
|
|
150
200
|
|
|
151
201
|
def _get_resource_url(self, path: str | None = None) -> AnyHttpUrl | None:
|
|
@@ -367,7 +417,7 @@ class OAuthProvider(
|
|
|
367
417
|
self.issuer_url is not None
|
|
368
418
|
) # typing check (issuer_url defaults to base_url)
|
|
369
419
|
|
|
370
|
-
|
|
420
|
+
sdk_routes = create_auth_routes(
|
|
371
421
|
provider=self,
|
|
372
422
|
issuer_url=self.base_url,
|
|
373
423
|
service_documentation_url=self.service_documentation_url,
|
|
@@ -375,6 +425,32 @@ class OAuthProvider(
|
|
|
375
425
|
revocation_options=self.revocation_options,
|
|
376
426
|
)
|
|
377
427
|
|
|
428
|
+
# Replace the token endpoint with our custom handler that returns
|
|
429
|
+
# proper OAuth 2.1 error codes (invalid_client instead of unauthorized_client)
|
|
430
|
+
oauth_routes: list[Route] = []
|
|
431
|
+
for route in sdk_routes:
|
|
432
|
+
if (
|
|
433
|
+
isinstance(route, Route)
|
|
434
|
+
and route.path == "/token"
|
|
435
|
+
and route.methods is not None
|
|
436
|
+
and "POST" in route.methods
|
|
437
|
+
):
|
|
438
|
+
# Replace with our OAuth 2.1 compliant token handler
|
|
439
|
+
token_handler = TokenHandler(
|
|
440
|
+
provider=self, client_authenticator=ClientAuthenticator(self)
|
|
441
|
+
)
|
|
442
|
+
oauth_routes.append(
|
|
443
|
+
Route(
|
|
444
|
+
path="/token",
|
|
445
|
+
endpoint=cors_middleware(
|
|
446
|
+
token_handler.handle, ["POST", "OPTIONS"]
|
|
447
|
+
),
|
|
448
|
+
methods=["POST", "OPTIONS"],
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
oauth_routes.append(route)
|
|
453
|
+
|
|
378
454
|
# Get the resource URL based on the MCP path
|
|
379
455
|
resource_url = self._get_resource_url(mcp_path)
|
|
380
456
|
|
|
@@ -397,3 +473,51 @@ class OAuthProvider(
|
|
|
397
473
|
oauth_routes.extend(super().get_routes(mcp_path))
|
|
398
474
|
|
|
399
475
|
return oauth_routes
|
|
476
|
+
|
|
477
|
+
def get_well_known_routes(
|
|
478
|
+
self,
|
|
479
|
+
mcp_path: str | None = None,
|
|
480
|
+
) -> list[Route]:
|
|
481
|
+
"""Get well-known discovery routes with RFC 8414 path-aware support.
|
|
482
|
+
|
|
483
|
+
Overrides the base implementation to support path-aware authorization
|
|
484
|
+
server metadata discovery per RFC 8414. If issuer_url has a path component,
|
|
485
|
+
the authorization server metadata route is adjusted to include that path.
|
|
486
|
+
|
|
487
|
+
For example, if issuer_url is "http://example.com/api", the discovery
|
|
488
|
+
endpoint will be at "/.well-known/oauth-authorization-server/api" instead
|
|
489
|
+
of just "/.well-known/oauth-authorization-server".
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
List of well-known discovery routes
|
|
496
|
+
"""
|
|
497
|
+
routes = super().get_well_known_routes(mcp_path)
|
|
498
|
+
|
|
499
|
+
# RFC 8414: If issuer_url has a path, use path-aware discovery
|
|
500
|
+
if self.issuer_url:
|
|
501
|
+
parsed = urlparse(str(self.issuer_url))
|
|
502
|
+
issuer_path = parsed.path.rstrip("/")
|
|
503
|
+
|
|
504
|
+
if issuer_path and issuer_path != "/":
|
|
505
|
+
# Replace /.well-known/oauth-authorization-server with path-aware version
|
|
506
|
+
new_routes = []
|
|
507
|
+
for route in routes:
|
|
508
|
+
if route.path == "/.well-known/oauth-authorization-server":
|
|
509
|
+
new_path = (
|
|
510
|
+
f"/.well-known/oauth-authorization-server{issuer_path}"
|
|
511
|
+
)
|
|
512
|
+
new_routes.append(
|
|
513
|
+
Route(
|
|
514
|
+
new_path,
|
|
515
|
+
endpoint=route.endpoint,
|
|
516
|
+
methods=route.methods,
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
else:
|
|
520
|
+
new_routes.append(route)
|
|
521
|
+
return new_routes
|
|
522
|
+
|
|
523
|
+
return routes
|