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/server/server.py
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import inspect
|
|
6
|
-
import json
|
|
7
7
|
import re
|
|
8
8
|
import secrets
|
|
9
9
|
import warnings
|
|
10
|
+
import weakref
|
|
10
11
|
from collections.abc import (
|
|
11
12
|
AsyncIterator,
|
|
12
13
|
Awaitable,
|
|
@@ -19,6 +20,7 @@ from contextlib import (
|
|
|
19
20
|
AbstractAsyncContextManager,
|
|
20
21
|
AsyncExitStack,
|
|
21
22
|
asynccontextmanager,
|
|
23
|
+
suppress,
|
|
22
24
|
)
|
|
23
25
|
from dataclasses import dataclass
|
|
24
26
|
from functools import partial
|
|
@@ -29,14 +31,18 @@ import anyio
|
|
|
29
31
|
import httpx
|
|
30
32
|
import mcp.types
|
|
31
33
|
import uvicorn
|
|
34
|
+
from docket import Docket, Worker
|
|
32
35
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
33
36
|
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
34
37
|
from mcp.server.stdio import stdio_server
|
|
38
|
+
from mcp.shared.exceptions import McpError
|
|
35
39
|
from mcp.types import (
|
|
40
|
+
METHOD_NOT_FOUND,
|
|
36
41
|
Annotations,
|
|
37
42
|
AnyFunction,
|
|
38
43
|
CallToolRequestParams,
|
|
39
44
|
ContentBlock,
|
|
45
|
+
ErrorData,
|
|
40
46
|
GetPromptResult,
|
|
41
47
|
ToolAnnotations,
|
|
42
48
|
)
|
|
@@ -57,10 +63,11 @@ from fastmcp.mcp_config import MCPConfig
|
|
|
57
63
|
from fastmcp.prompts import Prompt
|
|
58
64
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
59
65
|
from fastmcp.prompts.prompt_manager import PromptManager
|
|
60
|
-
from fastmcp.resources.resource import Resource
|
|
66
|
+
from fastmcp.resources.resource import FunctionResource, Resource
|
|
61
67
|
from fastmcp.resources.resource_manager import ResourceManager
|
|
62
|
-
from fastmcp.resources.template import ResourceTemplate
|
|
68
|
+
from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
|
|
63
69
|
from fastmcp.server.auth import AuthProvider
|
|
70
|
+
from fastmcp.server.event_store import EventStore
|
|
64
71
|
from fastmcp.server.http import (
|
|
65
72
|
StarletteWithLifespan,
|
|
66
73
|
create_sse_app,
|
|
@@ -68,6 +75,13 @@ from fastmcp.server.http import (
|
|
|
68
75
|
)
|
|
69
76
|
from fastmcp.server.low_level import LowLevelServer
|
|
70
77
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
78
|
+
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
79
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
80
|
+
from fastmcp.server.tasks.handlers import (
|
|
81
|
+
handle_prompt_as_task,
|
|
82
|
+
handle_resource_as_task,
|
|
83
|
+
handle_tool_as_task,
|
|
84
|
+
)
|
|
71
85
|
from fastmcp.settings import Settings
|
|
72
86
|
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
73
87
|
from fastmcp.tools.tool_manager import ToolManager
|
|
@@ -81,14 +95,6 @@ if TYPE_CHECKING:
|
|
|
81
95
|
from fastmcp.client import Client
|
|
82
96
|
from fastmcp.client.client import FastMCP1Server
|
|
83
97
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
84
|
-
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
85
|
-
from fastmcp.experimental.server.openapi.routing import (
|
|
86
|
-
ComponentFn as OpenAPIComponentFnNew,
|
|
87
|
-
)
|
|
88
|
-
from fastmcp.experimental.server.openapi.routing import RouteMap as RouteMapNew
|
|
89
|
-
from fastmcp.experimental.server.openapi.routing import (
|
|
90
|
-
RouteMapFn as OpenAPIRouteMapFnNew,
|
|
91
|
-
)
|
|
92
98
|
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
93
99
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
94
100
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
@@ -98,6 +104,24 @@ if TYPE_CHECKING:
|
|
|
98
104
|
|
|
99
105
|
logger = get_logger(__name__)
|
|
100
106
|
|
|
107
|
+
|
|
108
|
+
def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
|
|
109
|
+
"""Create a wrapper function with a custom __name__ for Docket registration.
|
|
110
|
+
|
|
111
|
+
Docket uses fn.__name__ as the key for function registration and lookup.
|
|
112
|
+
When mounting servers, we need unique names to avoid collisions between
|
|
113
|
+
mounted servers that have identically-named functions.
|
|
114
|
+
"""
|
|
115
|
+
import functools
|
|
116
|
+
|
|
117
|
+
@functools.wraps(fn)
|
|
118
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
119
|
+
return await fn(*args, **kwargs)
|
|
120
|
+
|
|
121
|
+
wrapper.__name__ = name
|
|
122
|
+
return wrapper
|
|
123
|
+
|
|
124
|
+
|
|
101
125
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
102
126
|
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
103
127
|
|
|
@@ -158,8 +182,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
158
182
|
auth: AuthProvider | NotSetT | None = NotSet,
|
|
159
183
|
middleware: Sequence[Middleware] | None = None,
|
|
160
184
|
lifespan: LifespanCallable | None = None,
|
|
161
|
-
dependencies: list[str] | None = None,
|
|
162
|
-
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
163
185
|
mask_error_details: bool | None = None,
|
|
164
186
|
tools: Sequence[Tool | Callable[..., Any]] | None = None,
|
|
165
187
|
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
@@ -171,6 +193,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
171
193
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
172
194
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
173
195
|
strict_input_validation: bool | None = None,
|
|
196
|
+
tasks: bool | None = None,
|
|
174
197
|
# ---
|
|
175
198
|
# ---
|
|
176
199
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -188,12 +211,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
188
211
|
sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
|
|
189
212
|
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
190
213
|
):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
# Resolve server default for background task support
|
|
215
|
+
self._support_tasks_by_default: bool = tasks if tasks is not None else False
|
|
216
|
+
|
|
217
|
+
# Docket instance (set during lifespan for cross-task access)
|
|
218
|
+
self._docket = None
|
|
194
219
|
|
|
195
220
|
self._additional_http_routes: list[BaseRoute] = []
|
|
196
221
|
self._mounted_servers: list[MountedServer] = []
|
|
222
|
+
self._is_mounted: bool = (
|
|
223
|
+
False # Set to True when this server is mounted on another
|
|
224
|
+
)
|
|
197
225
|
self._tool_manager: ToolManager = ToolManager(
|
|
198
226
|
duplicate_behavior=on_duplicate_tools,
|
|
199
227
|
mask_error_details=mask_error_details,
|
|
@@ -212,6 +240,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
212
240
|
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
|
|
213
241
|
self._lifespan_result: LifespanResultT | None = None
|
|
214
242
|
self._lifespan_result_set: bool = False
|
|
243
|
+
self._started: asyncio.Event = asyncio.Event()
|
|
215
244
|
|
|
216
245
|
# Generate random ID if no name provided
|
|
217
246
|
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
|
|
@@ -259,24 +288,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
259
288
|
# Set up MCP protocol handlers
|
|
260
289
|
self._setup_handlers()
|
|
261
290
|
|
|
262
|
-
# Handle dependencies with deprecation warning
|
|
263
|
-
# TODO: Remove dependencies parameter (deprecated in v2.11.4)
|
|
264
|
-
if dependencies is not None:
|
|
265
|
-
import warnings
|
|
266
|
-
|
|
267
|
-
warnings.warn(
|
|
268
|
-
"The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
|
|
269
|
-
"Please specify dependencies in a fastmcp.json configuration file instead:\n"
|
|
270
|
-
'{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
|
|
271
|
-
f"{json.dumps(dependencies)}\n }}\n}}\n"
|
|
272
|
-
"See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
|
|
273
|
-
DeprecationWarning,
|
|
274
|
-
stacklevel=2,
|
|
275
|
-
)
|
|
276
|
-
self.dependencies: list[str] = (
|
|
277
|
-
dependencies or fastmcp.settings.server_dependencies
|
|
278
|
-
) # TODO: Remove (deprecated in v2.11.4)
|
|
279
|
-
|
|
280
291
|
self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
|
|
281
292
|
sampling_handler
|
|
282
293
|
)
|
|
@@ -290,7 +301,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
290
301
|
else fastmcp.settings.include_fastmcp_meta
|
|
291
302
|
)
|
|
292
303
|
|
|
293
|
-
# handle deprecated settings
|
|
294
304
|
self._handle_deprecated_settings(
|
|
295
305
|
log_level=log_level,
|
|
296
306
|
debug=debug,
|
|
@@ -383,14 +393,197 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
383
393
|
else:
|
|
384
394
|
return list(self._mcp_server.icons)
|
|
385
395
|
|
|
396
|
+
@property
|
|
397
|
+
def docket(self) -> Docket | None:
|
|
398
|
+
"""Get the Docket instance if Docket support is enabled.
|
|
399
|
+
|
|
400
|
+
Returns None if Docket is not enabled or server hasn't been started yet.
|
|
401
|
+
"""
|
|
402
|
+
return self._docket
|
|
403
|
+
|
|
404
|
+
@asynccontextmanager
|
|
405
|
+
async def _docket_lifespan(self) -> AsyncIterator[None]:
|
|
406
|
+
"""Manage Docket instance and Worker for background task execution."""
|
|
407
|
+
from fastmcp import settings
|
|
408
|
+
|
|
409
|
+
# Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
|
|
410
|
+
from fastmcp.server.dependencies import (
|
|
411
|
+
_current_docket,
|
|
412
|
+
_current_server,
|
|
413
|
+
_current_worker,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
server_token = _current_server.set(weakref.ref(self))
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
# For directly mounted servers, the parent's Docket/Worker handles all
|
|
420
|
+
# task execution. Skip creating our own to avoid race conditions with
|
|
421
|
+
# multiple workers competing for tasks from the same queue.
|
|
422
|
+
if self._is_mounted:
|
|
423
|
+
yield
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
# Create Docket instance using configured name and URL
|
|
427
|
+
async with Docket(
|
|
428
|
+
name=settings.docket.name,
|
|
429
|
+
url=settings.docket.url,
|
|
430
|
+
) as docket:
|
|
431
|
+
# Store on server instance for cross-task access (FastMCPTransport)
|
|
432
|
+
self._docket = docket
|
|
433
|
+
|
|
434
|
+
# Register local task-enabled tools/prompts/resources with Docket
|
|
435
|
+
# Only function-based variants support background tasks
|
|
436
|
+
# Register components where task execution is not "forbidden"
|
|
437
|
+
for tool in self._tool_manager._tools.values():
|
|
438
|
+
if (
|
|
439
|
+
isinstance(tool, FunctionTool)
|
|
440
|
+
and tool.task_config.mode != "forbidden"
|
|
441
|
+
):
|
|
442
|
+
docket.register(tool.fn)
|
|
443
|
+
|
|
444
|
+
for prompt in self._prompt_manager._prompts.values():
|
|
445
|
+
if (
|
|
446
|
+
isinstance(prompt, FunctionPrompt)
|
|
447
|
+
and prompt.task_config.mode != "forbidden"
|
|
448
|
+
):
|
|
449
|
+
# task execution requires async fn (validated at creation time)
|
|
450
|
+
docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
|
|
451
|
+
|
|
452
|
+
for resource in self._resource_manager._resources.values():
|
|
453
|
+
if (
|
|
454
|
+
isinstance(resource, FunctionResource)
|
|
455
|
+
and resource.task_config.mode != "forbidden"
|
|
456
|
+
):
|
|
457
|
+
docket.register(resource.fn)
|
|
458
|
+
|
|
459
|
+
for template in self._resource_manager._templates.values():
|
|
460
|
+
if (
|
|
461
|
+
isinstance(template, FunctionResourceTemplate)
|
|
462
|
+
and template.task_config.mode != "forbidden"
|
|
463
|
+
):
|
|
464
|
+
docket.register(template.fn)
|
|
465
|
+
|
|
466
|
+
# Also register functions from mounted servers so tasks can
|
|
467
|
+
# execute in the parent's Docket context
|
|
468
|
+
for mounted in self._mounted_servers:
|
|
469
|
+
await self._register_mounted_server_functions(
|
|
470
|
+
mounted.server, docket, mounted.prefix
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Set Docket in ContextVar so CurrentDocket can access it
|
|
474
|
+
docket_token = _current_docket.set(docket)
|
|
475
|
+
try:
|
|
476
|
+
# Build worker kwargs from settings
|
|
477
|
+
worker_kwargs: dict[str, Any] = {
|
|
478
|
+
"concurrency": settings.docket.concurrency,
|
|
479
|
+
"redelivery_timeout": settings.docket.redelivery_timeout,
|
|
480
|
+
"reconnection_delay": settings.docket.reconnection_delay,
|
|
481
|
+
}
|
|
482
|
+
if settings.docket.worker_name:
|
|
483
|
+
worker_kwargs["name"] = settings.docket.worker_name
|
|
484
|
+
|
|
485
|
+
# Create and start Worker
|
|
486
|
+
async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
|
|
487
|
+
# Set Worker in ContextVar so CurrentWorker can access it
|
|
488
|
+
worker_token = _current_worker.set(worker)
|
|
489
|
+
try:
|
|
490
|
+
worker_task = asyncio.create_task(worker.run_forever())
|
|
491
|
+
try:
|
|
492
|
+
yield
|
|
493
|
+
finally:
|
|
494
|
+
# Cancel worker task on exit with timeout to prevent hanging
|
|
495
|
+
worker_task.cancel()
|
|
496
|
+
with suppress(
|
|
497
|
+
asyncio.CancelledError, asyncio.TimeoutError
|
|
498
|
+
):
|
|
499
|
+
await asyncio.wait_for(worker_task, timeout=2.0)
|
|
500
|
+
finally:
|
|
501
|
+
_current_worker.reset(worker_token)
|
|
502
|
+
finally:
|
|
503
|
+
# Reset ContextVar
|
|
504
|
+
_current_docket.reset(docket_token)
|
|
505
|
+
# Clear instance attribute
|
|
506
|
+
self._docket = None
|
|
507
|
+
finally:
|
|
508
|
+
# Reset server ContextVar
|
|
509
|
+
_current_server.reset(server_token)
|
|
510
|
+
|
|
511
|
+
async def _register_mounted_server_functions(
|
|
512
|
+
self, server: FastMCP, docket: Docket, prefix: str | None
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Register task-enabled functions from a mounted server with Docket.
|
|
515
|
+
|
|
516
|
+
This enables background task execution for mounted server components
|
|
517
|
+
through the parent server's Docket context.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
server: The mounted server whose functions to register
|
|
521
|
+
docket: The Docket instance to register with
|
|
522
|
+
prefix: The mount prefix to prepend to function names (matches
|
|
523
|
+
client-facing tool/prompt names)
|
|
524
|
+
"""
|
|
525
|
+
# Register tools with prefixed names to avoid collisions
|
|
526
|
+
for tool in server._tool_manager._tools.values():
|
|
527
|
+
if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
|
|
528
|
+
# Use same naming as client-facing tool keys
|
|
529
|
+
fn_name = f"{prefix}_{tool.key}" if prefix else tool.key
|
|
530
|
+
named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
|
|
531
|
+
docket.register(named_fn)
|
|
532
|
+
|
|
533
|
+
# Register prompts with prefixed names
|
|
534
|
+
for prompt in server._prompt_manager._prompts.values():
|
|
535
|
+
if (
|
|
536
|
+
isinstance(prompt, FunctionPrompt)
|
|
537
|
+
and prompt.task_config.mode != "forbidden"
|
|
538
|
+
):
|
|
539
|
+
fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
|
|
540
|
+
named_fn = _create_named_fn_wrapper(
|
|
541
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
|
|
542
|
+
)
|
|
543
|
+
docket.register(named_fn)
|
|
544
|
+
|
|
545
|
+
# Register resources with prefixed names (use name, not key/URI)
|
|
546
|
+
for resource in server._resource_manager._resources.values():
|
|
547
|
+
if (
|
|
548
|
+
isinstance(resource, FunctionResource)
|
|
549
|
+
and resource.task_config.mode != "forbidden"
|
|
550
|
+
):
|
|
551
|
+
fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
|
|
552
|
+
named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
|
|
553
|
+
docket.register(named_fn)
|
|
554
|
+
|
|
555
|
+
# Register resource templates with prefixed names (use name, not key/URI)
|
|
556
|
+
for template in server._resource_manager._templates.values():
|
|
557
|
+
if (
|
|
558
|
+
isinstance(template, FunctionResourceTemplate)
|
|
559
|
+
and template.task_config.mode != "forbidden"
|
|
560
|
+
):
|
|
561
|
+
fn_name = f"{prefix}_{template.name}" if prefix else template.name
|
|
562
|
+
named_fn = _create_named_fn_wrapper(template.fn, fn_name)
|
|
563
|
+
docket.register(named_fn)
|
|
564
|
+
|
|
565
|
+
# Recursively register from nested mounted servers with accumulated prefix
|
|
566
|
+
for nested in server._mounted_servers:
|
|
567
|
+
nested_prefix = (
|
|
568
|
+
f"{prefix}_{nested.prefix}"
|
|
569
|
+
if prefix and nested.prefix
|
|
570
|
+
else (prefix or nested.prefix)
|
|
571
|
+
)
|
|
572
|
+
await self._register_mounted_server_functions(
|
|
573
|
+
nested.server, docket, nested_prefix
|
|
574
|
+
)
|
|
575
|
+
|
|
386
576
|
@asynccontextmanager
|
|
387
577
|
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
388
578
|
if self._lifespan_result_set:
|
|
389
579
|
yield
|
|
390
580
|
return
|
|
391
581
|
|
|
392
|
-
async with
|
|
393
|
-
self.
|
|
582
|
+
async with (
|
|
583
|
+
self._lifespan(self) as user_lifespan_result,
|
|
584
|
+
self._docket_lifespan(),
|
|
585
|
+
):
|
|
586
|
+
self._lifespan_result = user_lifespan_result
|
|
394
587
|
self._lifespan_result_set = True
|
|
395
588
|
|
|
396
589
|
async with AsyncExitStack[bool | None]() as stack:
|
|
@@ -399,7 +592,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
399
592
|
cm=server.server._lifespan_manager()
|
|
400
593
|
)
|
|
401
594
|
|
|
402
|
-
|
|
595
|
+
self._started.set()
|
|
596
|
+
try:
|
|
597
|
+
yield
|
|
598
|
+
finally:
|
|
599
|
+
self._started.clear()
|
|
403
600
|
|
|
404
601
|
self._lifespan_result_set = False
|
|
405
602
|
self._lifespan_result = None
|
|
@@ -464,8 +661,260 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
464
661
|
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
465
662
|
self._call_tool_mcp
|
|
466
663
|
)
|
|
467
|
-
|
|
468
|
-
self.
|
|
664
|
+
# Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
|
|
665
|
+
self._setup_read_resource_handler()
|
|
666
|
+
# Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
|
|
667
|
+
self._setup_get_prompt_handler()
|
|
668
|
+
# Register custom SEP-1686 task protocol handlers
|
|
669
|
+
self._setup_task_protocol_handlers()
|
|
670
|
+
|
|
671
|
+
def _setup_read_resource_handler(self) -> None:
|
|
672
|
+
"""
|
|
673
|
+
Set up custom read_resource handler that supports task-augmented responses.
|
|
674
|
+
|
|
675
|
+
The SDK's read_resource decorator doesn't support CreateTaskResult returns,
|
|
676
|
+
so we register a custom handler that checks request_context.experimental.is_task.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
|
|
680
|
+
uri = req.params.uri
|
|
681
|
+
|
|
682
|
+
# Check for task metadata via SDK's request context
|
|
683
|
+
task_meta = None
|
|
684
|
+
try:
|
|
685
|
+
ctx = self._mcp_server.request_context
|
|
686
|
+
if ctx.experimental.is_task:
|
|
687
|
+
task_meta = ctx.experimental.task_metadata
|
|
688
|
+
except (AttributeError, LookupError):
|
|
689
|
+
pass
|
|
690
|
+
|
|
691
|
+
# Check for task metadata and route appropriately
|
|
692
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
693
|
+
# Get resource including from mounted servers
|
|
694
|
+
resource = await self._get_resource_or_template_or_none(str(uri))
|
|
695
|
+
if (
|
|
696
|
+
resource
|
|
697
|
+
and self._should_enable_component(resource)
|
|
698
|
+
and hasattr(resource, "task_config")
|
|
699
|
+
):
|
|
700
|
+
task_mode = resource.task_config.mode # type: ignore[union-attr]
|
|
701
|
+
|
|
702
|
+
# Enforce mode="required" - must have task metadata
|
|
703
|
+
if task_mode == "required" and not task_meta:
|
|
704
|
+
raise McpError(
|
|
705
|
+
ErrorData(
|
|
706
|
+
code=METHOD_NOT_FOUND,
|
|
707
|
+
message=f"Resource '{uri}' requires task-augmented execution",
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Route to background if task metadata present and mode allows
|
|
712
|
+
if task_meta and task_mode != "forbidden":
|
|
713
|
+
# For FunctionResource/FunctionResourceTemplate, use Docket
|
|
714
|
+
if isinstance(
|
|
715
|
+
resource,
|
|
716
|
+
FunctionResource | FunctionResourceTemplate,
|
|
717
|
+
):
|
|
718
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
719
|
+
return await handle_resource_as_task(
|
|
720
|
+
self, str(uri), resource, task_meta_dict
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
724
|
+
# Raise error since resources don't have isError field
|
|
725
|
+
if task_meta and task_mode == "forbidden":
|
|
726
|
+
raise McpError(
|
|
727
|
+
ErrorData(
|
|
728
|
+
code=METHOD_NOT_FOUND,
|
|
729
|
+
message=f"Resource '{uri}' does not support task-augmented execution",
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# Synchronous execution
|
|
734
|
+
result = await self._read_resource_mcp(uri)
|
|
735
|
+
|
|
736
|
+
# Graceful degradation: if we got here with task_meta, something went wrong
|
|
737
|
+
# (This should be unreachable now that forbidden raises)
|
|
738
|
+
if task_meta:
|
|
739
|
+
mcp_contents = []
|
|
740
|
+
for item in result:
|
|
741
|
+
if isinstance(item.content, str):
|
|
742
|
+
mcp_contents.append(
|
|
743
|
+
mcp.types.TextResourceContents(
|
|
744
|
+
uri=uri,
|
|
745
|
+
text=item.content,
|
|
746
|
+
mimeType=item.mime_type or "text/plain",
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
elif isinstance(item.content, bytes):
|
|
750
|
+
import base64
|
|
751
|
+
|
|
752
|
+
mcp_contents.append(
|
|
753
|
+
mcp.types.BlobResourceContents(
|
|
754
|
+
uri=uri,
|
|
755
|
+
blob=base64.b64encode(item.content).decode(),
|
|
756
|
+
mimeType=item.mime_type or "application/octet-stream",
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
return mcp.types.ServerResult(
|
|
760
|
+
mcp.types.ReadResourceResult(
|
|
761
|
+
contents=mcp_contents,
|
|
762
|
+
_meta={
|
|
763
|
+
"modelcontextprotocol.io/task": {
|
|
764
|
+
"returned_immediately": True
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Convert to proper ServerResult
|
|
771
|
+
if isinstance(result, mcp.types.ServerResult):
|
|
772
|
+
return result
|
|
773
|
+
|
|
774
|
+
mcp_contents = []
|
|
775
|
+
for item in result:
|
|
776
|
+
if isinstance(item.content, str):
|
|
777
|
+
mcp_contents.append(
|
|
778
|
+
mcp.types.TextResourceContents(
|
|
779
|
+
uri=uri,
|
|
780
|
+
text=item.content,
|
|
781
|
+
mimeType=item.mime_type or "text/plain",
|
|
782
|
+
)
|
|
783
|
+
)
|
|
784
|
+
elif isinstance(item.content, bytes):
|
|
785
|
+
import base64
|
|
786
|
+
|
|
787
|
+
mcp_contents.append(
|
|
788
|
+
mcp.types.BlobResourceContents(
|
|
789
|
+
uri=uri,
|
|
790
|
+
blob=base64.b64encode(item.content).decode(),
|
|
791
|
+
mimeType=item.mime_type or "application/octet-stream",
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
return mcp.types.ServerResult(
|
|
796
|
+
mcp.types.ReadResourceResult(contents=mcp_contents)
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
|
|
800
|
+
|
|
801
|
+
def _setup_get_prompt_handler(self) -> None:
|
|
802
|
+
"""
|
|
803
|
+
Set up custom get_prompt handler that supports task-augmented responses.
|
|
804
|
+
|
|
805
|
+
The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
|
|
806
|
+
so we register a custom handler that checks request_context.experimental.is_task.
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
|
|
810
|
+
name = req.params.name
|
|
811
|
+
arguments = req.params.arguments
|
|
812
|
+
|
|
813
|
+
# Check for task metadata via SDK's request context
|
|
814
|
+
task_meta = None
|
|
815
|
+
try:
|
|
816
|
+
ctx = self._mcp_server.request_context
|
|
817
|
+
if ctx.experimental.is_task:
|
|
818
|
+
task_meta = ctx.experimental.task_metadata
|
|
819
|
+
except (AttributeError, LookupError):
|
|
820
|
+
pass
|
|
821
|
+
|
|
822
|
+
# Check for task metadata and route appropriately
|
|
823
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
824
|
+
prompts = await self.get_prompts()
|
|
825
|
+
prompt = prompts.get(name)
|
|
826
|
+
if (
|
|
827
|
+
prompt
|
|
828
|
+
and self._should_enable_component(prompt)
|
|
829
|
+
and hasattr(prompt, "task_config")
|
|
830
|
+
and prompt.task_config
|
|
831
|
+
):
|
|
832
|
+
task_mode = prompt.task_config.mode # type: ignore[union-attr]
|
|
833
|
+
|
|
834
|
+
# Enforce mode="required" - must have task metadata
|
|
835
|
+
if task_mode == "required" and not task_meta:
|
|
836
|
+
raise McpError(
|
|
837
|
+
ErrorData(
|
|
838
|
+
code=METHOD_NOT_FOUND,
|
|
839
|
+
message=f"Prompt '{name}' requires task-augmented execution",
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
# Route to background if task metadata present and mode allows
|
|
844
|
+
if task_meta and task_mode != "forbidden":
|
|
845
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
846
|
+
result = await handle_prompt_as_task(
|
|
847
|
+
self, name, arguments, task_meta_dict
|
|
848
|
+
)
|
|
849
|
+
return mcp.types.ServerResult(result)
|
|
850
|
+
|
|
851
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
852
|
+
# Raise error since prompts don't have isError field
|
|
853
|
+
if task_meta and task_mode == "forbidden":
|
|
854
|
+
raise McpError(
|
|
855
|
+
ErrorData(
|
|
856
|
+
code=METHOD_NOT_FOUND,
|
|
857
|
+
message=f"Prompt '{name}' does not support task-augmented execution",
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
# Synchronous execution
|
|
862
|
+
result = await self._get_prompt_mcp(name, arguments)
|
|
863
|
+
return mcp.types.ServerResult(result)
|
|
864
|
+
|
|
865
|
+
self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
|
|
866
|
+
|
|
867
|
+
def _setup_task_protocol_handlers(self) -> None:
|
|
868
|
+
"""Register SEP-1686 task protocol handlers with SDK."""
|
|
869
|
+
from mcp.types import (
|
|
870
|
+
CancelTaskRequest,
|
|
871
|
+
GetTaskPayloadRequest,
|
|
872
|
+
GetTaskRequest,
|
|
873
|
+
ListTasksRequest,
|
|
874
|
+
ServerResult,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
from fastmcp.server.tasks.protocol import (
|
|
878
|
+
tasks_cancel_handler,
|
|
879
|
+
tasks_get_handler,
|
|
880
|
+
tasks_list_handler,
|
|
881
|
+
tasks_result_handler,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# Manually register handlers (SDK decorators fail with locally-defined functions)
|
|
885
|
+
# SDK expects handlers that receive Request objects and return ServerResult
|
|
886
|
+
|
|
887
|
+
async def handle_get_task(req: GetTaskRequest) -> ServerResult:
|
|
888
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
889
|
+
result = await tasks_get_handler(self, params)
|
|
890
|
+
return ServerResult(result)
|
|
891
|
+
|
|
892
|
+
async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
|
|
893
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
894
|
+
result = await tasks_result_handler(self, params)
|
|
895
|
+
return ServerResult(result)
|
|
896
|
+
|
|
897
|
+
async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
|
|
898
|
+
params = (
|
|
899
|
+
req.params.model_dump(by_alias=True, exclude_none=True)
|
|
900
|
+
if req.params
|
|
901
|
+
else {}
|
|
902
|
+
)
|
|
903
|
+
result = await tasks_list_handler(self, params)
|
|
904
|
+
return ServerResult(result)
|
|
905
|
+
|
|
906
|
+
async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
|
|
907
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
908
|
+
result = await tasks_cancel_handler(self, params)
|
|
909
|
+
return ServerResult(result)
|
|
910
|
+
|
|
911
|
+
# Register directly with SDK (same as what decorators do internally)
|
|
912
|
+
self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
|
|
913
|
+
self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
|
|
914
|
+
handle_get_task_result
|
|
915
|
+
)
|
|
916
|
+
self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
|
|
917
|
+
self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
|
|
469
918
|
|
|
470
919
|
async def _apply_middleware(
|
|
471
920
|
self,
|
|
@@ -507,6 +956,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
507
956
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
508
957
|
return tools[key]
|
|
509
958
|
|
|
959
|
+
async def _get_tool_with_task_config(self, key: str) -> Tool | None:
|
|
960
|
+
"""Get a tool by key, returning None if not found.
|
|
961
|
+
|
|
962
|
+
Used for task config checking where we need the actual tool object
|
|
963
|
+
(including from mounted servers and proxies) but don't want to raise.
|
|
964
|
+
"""
|
|
965
|
+
try:
|
|
966
|
+
return await self.get_tool(key)
|
|
967
|
+
except NotFoundError:
|
|
968
|
+
return None
|
|
969
|
+
|
|
970
|
+
async def _get_resource_or_template_or_none(
|
|
971
|
+
self, uri: str
|
|
972
|
+
) -> Resource | ResourceTemplate | None:
|
|
973
|
+
"""Get a resource or template by URI, searching recursively. Returns None if not found."""
|
|
974
|
+
try:
|
|
975
|
+
return await self.get_resource(uri)
|
|
976
|
+
except NotFoundError:
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
templates = await self.get_resource_templates()
|
|
980
|
+
for template in templates.values():
|
|
981
|
+
if template.matches(uri):
|
|
982
|
+
return template
|
|
983
|
+
|
|
984
|
+
return None
|
|
985
|
+
|
|
510
986
|
async def get_resources(self) -> dict[str, Resource]:
|
|
511
987
|
"""Get all resources (unfiltered), including mounted servers, indexed by key."""
|
|
512
988
|
all_resources = dict(await self._resource_manager.get_resources())
|
|
@@ -516,9 +992,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
516
992
|
child_resources = await mounted.server.get_resources()
|
|
517
993
|
for key, resource in child_resources.items():
|
|
518
994
|
new_key = (
|
|
519
|
-
add_resource_prefix(
|
|
520
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
521
|
-
)
|
|
995
|
+
add_resource_prefix(key, mounted.prefix)
|
|
522
996
|
if mounted.prefix
|
|
523
997
|
else key
|
|
524
998
|
)
|
|
@@ -555,17 +1029,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
555
1029
|
child_templates = await mounted.server.get_resource_templates()
|
|
556
1030
|
for key, template in child_templates.items():
|
|
557
1031
|
new_key = (
|
|
558
|
-
add_resource_prefix(
|
|
559
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
560
|
-
)
|
|
1032
|
+
add_resource_prefix(key, mounted.prefix)
|
|
561
1033
|
if mounted.prefix
|
|
562
1034
|
else key
|
|
563
1035
|
)
|
|
564
|
-
update =
|
|
565
|
-
|
|
566
|
-
if
|
|
567
|
-
|
|
568
|
-
|
|
1036
|
+
update: dict[str, Any] = {}
|
|
1037
|
+
if mounted.prefix:
|
|
1038
|
+
if template.name:
|
|
1039
|
+
update["name"] = f"{mounted.prefix}_{template.name}"
|
|
1040
|
+
# Update uri_template so matches() works with prefixed URIs
|
|
1041
|
+
update["uri_template"] = new_key
|
|
569
1042
|
all_templates[new_key] = template.model_copy(
|
|
570
1043
|
key=new_key, update=update
|
|
571
1044
|
)
|
|
@@ -835,11 +1308,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
835
1308
|
|
|
836
1309
|
key = resource.key
|
|
837
1310
|
if mounted.prefix:
|
|
838
|
-
key = add_resource_prefix(
|
|
839
|
-
resource.key,
|
|
840
|
-
mounted.prefix,
|
|
841
|
-
mounted.resource_prefix_format,
|
|
842
|
-
)
|
|
1311
|
+
key = add_resource_prefix(resource.key, mounted.prefix)
|
|
843
1312
|
resource = resource.model_copy(
|
|
844
1313
|
key=key,
|
|
845
1314
|
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
@@ -931,11 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
931
1400
|
|
|
932
1401
|
key = template.key
|
|
933
1402
|
if mounted.prefix:
|
|
934
|
-
key = add_resource_prefix(
|
|
935
|
-
template.key,
|
|
936
|
-
mounted.prefix,
|
|
937
|
-
mounted.resource_prefix_format,
|
|
938
|
-
)
|
|
1403
|
+
key = add_resource_prefix(template.key, mounted.prefix)
|
|
939
1404
|
template = template.model_copy(
|
|
940
1405
|
key=key,
|
|
941
1406
|
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
@@ -1054,7 +1519,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1054
1519
|
"""
|
|
1055
1520
|
Handle MCP 'callTool' requests.
|
|
1056
1521
|
|
|
1057
|
-
|
|
1522
|
+
Detects SEP-1686 task metadata and routes to background execution if supported.
|
|
1058
1523
|
|
|
1059
1524
|
Args:
|
|
1060
1525
|
key: The name of the tool to call
|
|
@@ -1069,6 +1534,65 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1069
1534
|
|
|
1070
1535
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1071
1536
|
try:
|
|
1537
|
+
# Check for SEP-1686 task metadata via request context
|
|
1538
|
+
task_meta = None
|
|
1539
|
+
try:
|
|
1540
|
+
# Access task metadata from SDK's request context
|
|
1541
|
+
ctx = self._mcp_server.request_context
|
|
1542
|
+
if ctx.experimental.is_task:
|
|
1543
|
+
task_meta = ctx.experimental.task_metadata
|
|
1544
|
+
except (AttributeError, LookupError):
|
|
1545
|
+
# No request context available - proceed without task metadata
|
|
1546
|
+
pass
|
|
1547
|
+
|
|
1548
|
+
# Get tool from local manager, mounted servers, or proxy
|
|
1549
|
+
tool = await self._get_tool_with_task_config(key)
|
|
1550
|
+
if (
|
|
1551
|
+
tool
|
|
1552
|
+
and self._should_enable_component(tool)
|
|
1553
|
+
and hasattr(tool, "task_config")
|
|
1554
|
+
):
|
|
1555
|
+
task_mode = tool.task_config.mode # type: ignore[union-attr]
|
|
1556
|
+
|
|
1557
|
+
# Enforce mode="required" - must have task metadata
|
|
1558
|
+
if task_mode == "required" and not task_meta:
|
|
1559
|
+
raise McpError(
|
|
1560
|
+
ErrorData(
|
|
1561
|
+
code=METHOD_NOT_FOUND,
|
|
1562
|
+
message=f"Tool '{key}' requires task-augmented execution",
|
|
1563
|
+
)
|
|
1564
|
+
)
|
|
1565
|
+
|
|
1566
|
+
# Route to background if task metadata present and mode allows
|
|
1567
|
+
if task_meta and task_mode != "forbidden":
|
|
1568
|
+
# For FunctionTool, use Docket for background execution
|
|
1569
|
+
if isinstance(tool, FunctionTool):
|
|
1570
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
1571
|
+
return await handle_tool_as_task(
|
|
1572
|
+
self, key, arguments, task_meta_dict
|
|
1573
|
+
)
|
|
1574
|
+
# For ProxyTool/mounted tools, proceed with normal execution
|
|
1575
|
+
# They will forward task metadata to their backend
|
|
1576
|
+
|
|
1577
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
1578
|
+
# Return error result with returned_immediately=True
|
|
1579
|
+
if task_meta and task_mode == "forbidden":
|
|
1580
|
+
return mcp.types.CallToolResult(
|
|
1581
|
+
content=[
|
|
1582
|
+
mcp.types.TextContent(
|
|
1583
|
+
type="text",
|
|
1584
|
+
text=f"Tool '{key}' does not support task-augmented execution",
|
|
1585
|
+
)
|
|
1586
|
+
],
|
|
1587
|
+
isError=True,
|
|
1588
|
+
_meta={
|
|
1589
|
+
"modelcontextprotocol.io/task": {
|
|
1590
|
+
"returned_immediately": True
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
# Synchronous execution (normal path)
|
|
1072
1596
|
result = await self._call_tool_middleware(key, arguments)
|
|
1073
1597
|
return result.to_mcp_result()
|
|
1074
1598
|
except DisabledError as e:
|
|
@@ -1115,7 +1639,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1115
1639
|
|
|
1116
1640
|
try:
|
|
1117
1641
|
# First, get the tool to check if parent's filter allows it
|
|
1118
|
-
|
|
1642
|
+
# Use get_tool() instead of _tool_manager.get_tool() to support
|
|
1643
|
+
# nested mounted servers (tools mounted more than 2 levels deep)
|
|
1644
|
+
tool = await mounted.server.get_tool(try_name)
|
|
1119
1645
|
if not self._should_enable_component(tool):
|
|
1120
1646
|
# Parent filter blocks this tool, continue searching
|
|
1121
1647
|
continue
|
|
@@ -1148,6 +1674,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1148
1674
|
|
|
1149
1675
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1150
1676
|
try:
|
|
1677
|
+
# Task routing handled by custom handler
|
|
1151
1678
|
return list[ReadResourceContents](
|
|
1152
1679
|
await self._read_resource_middleware(uri)
|
|
1153
1680
|
)
|
|
@@ -1195,20 +1722,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1195
1722
|
for mounted in reversed(self._mounted_servers):
|
|
1196
1723
|
key = uri_str
|
|
1197
1724
|
if mounted.prefix:
|
|
1198
|
-
if not has_resource_prefix(
|
|
1199
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
1200
|
-
):
|
|
1725
|
+
if not has_resource_prefix(key, mounted.prefix):
|
|
1201
1726
|
continue
|
|
1202
|
-
key = remove_resource_prefix(
|
|
1203
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
1204
|
-
)
|
|
1727
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
1205
1728
|
|
|
1729
|
+
# First, get the resource/template to check if parent's filter allows it
|
|
1730
|
+
# Use get_resource_or_template to support nested mounted servers
|
|
1731
|
+
# (resources/templates mounted more than 2 levels deep)
|
|
1732
|
+
resource = await mounted.server._get_resource_or_template_or_none(key)
|
|
1733
|
+
if resource is None:
|
|
1734
|
+
continue
|
|
1735
|
+
if not self._should_enable_component(resource):
|
|
1736
|
+
# Parent filter blocks this resource, continue searching
|
|
1737
|
+
continue
|
|
1206
1738
|
try:
|
|
1207
|
-
# First, get the resource to check if parent's filter allows it
|
|
1208
|
-
resource = await mounted.server._resource_manager.get_resource(key)
|
|
1209
|
-
if not self._should_enable_component(resource):
|
|
1210
|
-
# Parent filter blocks this resource, continue searching
|
|
1211
|
-
continue
|
|
1212
1739
|
result = list(await mounted.server._read_resource_middleware(key))
|
|
1213
1740
|
return result
|
|
1214
1741
|
except NotFoundError:
|
|
@@ -1246,6 +1773,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1246
1773
|
|
|
1247
1774
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1248
1775
|
try:
|
|
1776
|
+
# Task routing handled by custom handler
|
|
1249
1777
|
return await self._get_prompt_middleware(name, arguments)
|
|
1250
1778
|
except DisabledError as e:
|
|
1251
1779
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
@@ -1288,7 +1816,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1288
1816
|
|
|
1289
1817
|
try:
|
|
1290
1818
|
# First, get the prompt to check if parent's filter allows it
|
|
1291
|
-
|
|
1819
|
+
# Use get_prompt() instead of _prompt_manager.get_prompt() to support
|
|
1820
|
+
# nested mounted servers (prompts mounted more than 2 levels deep)
|
|
1821
|
+
prompt = await mounted.server.get_prompt(try_name)
|
|
1292
1822
|
if not self._should_enable_component(prompt):
|
|
1293
1823
|
# Parent filter blocks this prompt, continue searching
|
|
1294
1824
|
continue
|
|
@@ -1380,6 +1910,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1380
1910
|
exclude_args: list[str] | None = None,
|
|
1381
1911
|
meta: dict[str, Any] | None = None,
|
|
1382
1912
|
enabled: bool | None = None,
|
|
1913
|
+
task: bool | TaskConfig | None = None,
|
|
1383
1914
|
) -> FunctionTool: ...
|
|
1384
1915
|
|
|
1385
1916
|
@overload
|
|
@@ -1397,6 +1928,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1397
1928
|
exclude_args: list[str] | None = None,
|
|
1398
1929
|
meta: dict[str, Any] | None = None,
|
|
1399
1930
|
enabled: bool | None = None,
|
|
1931
|
+
task: bool | TaskConfig | None = None,
|
|
1400
1932
|
) -> Callable[[AnyFunction], FunctionTool]: ...
|
|
1401
1933
|
|
|
1402
1934
|
def tool(
|
|
@@ -1413,6 +1945,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1413
1945
|
exclude_args: list[str] | None = None,
|
|
1414
1946
|
meta: dict[str, Any] | None = None,
|
|
1415
1947
|
enabled: bool | None = None,
|
|
1948
|
+
task: bool | TaskConfig | None = None,
|
|
1416
1949
|
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
|
1417
1950
|
"""Decorator to register a tool.
|
|
1418
1951
|
|
|
@@ -1486,6 +2019,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1486
2019
|
fn = name_or_fn
|
|
1487
2020
|
tool_name = name # Use keyword name if provided, otherwise None
|
|
1488
2021
|
|
|
2022
|
+
# Resolve task parameter
|
|
2023
|
+
supports_task: bool | TaskConfig = (
|
|
2024
|
+
task if task is not None else self._support_tasks_by_default
|
|
2025
|
+
)
|
|
2026
|
+
|
|
1489
2027
|
# Register the tool immediately and return the tool object
|
|
1490
2028
|
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
|
1491
2029
|
tool = Tool.from_function(
|
|
@@ -1501,6 +2039,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1501
2039
|
meta=meta,
|
|
1502
2040
|
serializer=self._tool_serializer,
|
|
1503
2041
|
enabled=enabled,
|
|
2042
|
+
task=supports_task,
|
|
1504
2043
|
)
|
|
1505
2044
|
self.add_tool(tool)
|
|
1506
2045
|
return tool
|
|
@@ -1534,6 +2073,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1534
2073
|
exclude_args=exclude_args,
|
|
1535
2074
|
meta=meta,
|
|
1536
2075
|
enabled=enabled,
|
|
2076
|
+
task=task,
|
|
1537
2077
|
)
|
|
1538
2078
|
|
|
1539
2079
|
def add_resource(self, resource: Resource) -> Resource:
|
|
@@ -1580,44 +2120,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1580
2120
|
|
|
1581
2121
|
return template
|
|
1582
2122
|
|
|
1583
|
-
def add_resource_fn(
|
|
1584
|
-
self,
|
|
1585
|
-
fn: AnyFunction,
|
|
1586
|
-
uri: str,
|
|
1587
|
-
name: str | None = None,
|
|
1588
|
-
description: str | None = None,
|
|
1589
|
-
mime_type: str | None = None,
|
|
1590
|
-
tags: set[str] | None = None,
|
|
1591
|
-
) -> None:
|
|
1592
|
-
"""Add a resource or template to the server from a function.
|
|
1593
|
-
|
|
1594
|
-
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
1595
|
-
has parameters, it will be registered as a template resource.
|
|
1596
|
-
|
|
1597
|
-
Args:
|
|
1598
|
-
fn: The function to register as a resource
|
|
1599
|
-
uri: The URI for the resource
|
|
1600
|
-
name: Optional name for the resource
|
|
1601
|
-
description: Optional description of the resource
|
|
1602
|
-
mime_type: Optional MIME type for the resource
|
|
1603
|
-
tags: Optional set of tags for categorizing the resource
|
|
1604
|
-
"""
|
|
1605
|
-
# deprecated since 2.7.0
|
|
1606
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1607
|
-
warnings.warn(
|
|
1608
|
-
"The add_resource_fn method is deprecated. Use the resource decorator instead.",
|
|
1609
|
-
DeprecationWarning,
|
|
1610
|
-
stacklevel=2,
|
|
1611
|
-
)
|
|
1612
|
-
self._resource_manager.add_resource_or_template_from_fn(
|
|
1613
|
-
fn=fn,
|
|
1614
|
-
uri=uri,
|
|
1615
|
-
name=name,
|
|
1616
|
-
description=description,
|
|
1617
|
-
mime_type=mime_type,
|
|
1618
|
-
tags=tags,
|
|
1619
|
-
)
|
|
1620
|
-
|
|
1621
2123
|
def resource(
|
|
1622
2124
|
self,
|
|
1623
2125
|
uri: str,
|
|
@@ -1631,6 +2133,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1631
2133
|
enabled: bool | None = None,
|
|
1632
2134
|
annotations: Annotations | dict[str, Any] | None = None,
|
|
1633
2135
|
meta: dict[str, Any] | None = None,
|
|
2136
|
+
task: bool | TaskConfig | None = None,
|
|
1634
2137
|
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
|
|
1635
2138
|
"""Decorator to register a function as a resource.
|
|
1636
2139
|
|
|
@@ -1695,8 +2198,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1695
2198
|
)
|
|
1696
2199
|
|
|
1697
2200
|
def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
|
|
1698
|
-
from fastmcp.server.context import Context
|
|
1699
|
-
|
|
1700
2201
|
if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
|
|
1701
2202
|
raise ValueError(
|
|
1702
2203
|
inspect.cleandoc(
|
|
@@ -1709,14 +2210,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1709
2210
|
)
|
|
1710
2211
|
)
|
|
1711
2212
|
|
|
2213
|
+
# Resolve task parameter
|
|
2214
|
+
supports_task: bool | TaskConfig = (
|
|
2215
|
+
task if task is not None else self._support_tasks_by_default
|
|
2216
|
+
)
|
|
2217
|
+
|
|
1712
2218
|
# Check if this should be a template
|
|
1713
2219
|
has_uri_params = "{" in uri and "}" in uri
|
|
1714
|
-
#
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
)
|
|
2220
|
+
# Use wrapper to check for user-facing parameters
|
|
2221
|
+
from fastmcp.server.dependencies import without_injected_parameters
|
|
2222
|
+
|
|
2223
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
2224
|
+
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
|
|
1720
2225
|
|
|
1721
2226
|
if has_uri_params or has_func_params:
|
|
1722
2227
|
template = ResourceTemplate.from_function(
|
|
@@ -1731,6 +2236,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1731
2236
|
enabled=enabled,
|
|
1732
2237
|
annotations=annotations,
|
|
1733
2238
|
meta=meta,
|
|
2239
|
+
task=supports_task,
|
|
1734
2240
|
)
|
|
1735
2241
|
self.add_template(template)
|
|
1736
2242
|
return template
|
|
@@ -1747,6 +2253,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1747
2253
|
enabled=enabled,
|
|
1748
2254
|
annotations=annotations,
|
|
1749
2255
|
meta=meta,
|
|
2256
|
+
task=supports_task,
|
|
1750
2257
|
)
|
|
1751
2258
|
self.add_resource(resource)
|
|
1752
2259
|
return resource
|
|
@@ -1792,6 +2299,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1792
2299
|
tags: set[str] | None = None,
|
|
1793
2300
|
enabled: bool | None = None,
|
|
1794
2301
|
meta: dict[str, Any] | None = None,
|
|
2302
|
+
task: bool | TaskConfig | None = None,
|
|
1795
2303
|
) -> FunctionPrompt: ...
|
|
1796
2304
|
|
|
1797
2305
|
@overload
|
|
@@ -1806,6 +2314,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1806
2314
|
tags: set[str] | None = None,
|
|
1807
2315
|
enabled: bool | None = None,
|
|
1808
2316
|
meta: dict[str, Any] | None = None,
|
|
2317
|
+
task: bool | TaskConfig | None = None,
|
|
1809
2318
|
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
|
1810
2319
|
|
|
1811
2320
|
def prompt(
|
|
@@ -1819,6 +2328,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1819
2328
|
tags: set[str] | None = None,
|
|
1820
2329
|
enabled: bool | None = None,
|
|
1821
2330
|
meta: dict[str, Any] | None = None,
|
|
2331
|
+
task: bool | TaskConfig | None = None,
|
|
1822
2332
|
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
|
|
1823
2333
|
"""Decorator to register a prompt.
|
|
1824
2334
|
|
|
@@ -1909,6 +2419,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1909
2419
|
fn = name_or_fn
|
|
1910
2420
|
prompt_name = name # Use keyword name if provided, otherwise None
|
|
1911
2421
|
|
|
2422
|
+
# Resolve task parameter
|
|
2423
|
+
supports_task: bool | TaskConfig = (
|
|
2424
|
+
task if task is not None else self._support_tasks_by_default
|
|
2425
|
+
)
|
|
2426
|
+
|
|
1912
2427
|
# Register the prompt immediately
|
|
1913
2428
|
prompt = Prompt.from_function(
|
|
1914
2429
|
fn=fn,
|
|
@@ -1919,6 +2434,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1919
2434
|
tags=tags,
|
|
1920
2435
|
enabled=enabled,
|
|
1921
2436
|
meta=meta,
|
|
2437
|
+
task=supports_task,
|
|
1922
2438
|
)
|
|
1923
2439
|
self.add_prompt(prompt)
|
|
1924
2440
|
|
|
@@ -1950,6 +2466,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1950
2466
|
tags=tags,
|
|
1951
2467
|
enabled=enabled,
|
|
1952
2468
|
meta=meta,
|
|
2469
|
+
task=task,
|
|
1953
2470
|
)
|
|
1954
2471
|
|
|
1955
2472
|
async def run_stdio_async(
|
|
@@ -1974,11 +2491,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1974
2491
|
logger.info(
|
|
1975
2492
|
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
1976
2493
|
)
|
|
2494
|
+
|
|
2495
|
+
# Build experimental capabilities
|
|
2496
|
+
experimental_capabilities = get_task_capabilities()
|
|
2497
|
+
|
|
1977
2498
|
await self._mcp_server.run(
|
|
1978
2499
|
read_stream,
|
|
1979
2500
|
write_stream,
|
|
1980
2501
|
self._mcp_server.create_initialization_options(
|
|
1981
|
-
NotificationOptions(
|
|
2502
|
+
notification_options=NotificationOptions(
|
|
2503
|
+
tools_changed=True
|
|
2504
|
+
),
|
|
2505
|
+
experimental_capabilities=experimental_capabilities,
|
|
1982
2506
|
),
|
|
1983
2507
|
)
|
|
1984
2508
|
|
|
@@ -2061,86 +2585,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2061
2585
|
|
|
2062
2586
|
await server.serve()
|
|
2063
2587
|
|
|
2064
|
-
async def run_sse_async(
|
|
2065
|
-
self,
|
|
2066
|
-
host: str | None = None,
|
|
2067
|
-
port: int | None = None,
|
|
2068
|
-
log_level: str | None = None,
|
|
2069
|
-
path: str | None = None,
|
|
2070
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
2071
|
-
) -> None:
|
|
2072
|
-
"""Run the server using SSE transport."""
|
|
2073
|
-
|
|
2074
|
-
# Deprecated since 2.3.2
|
|
2075
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2076
|
-
warnings.warn(
|
|
2077
|
-
"The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
|
|
2078
|
-
"modern (non-SSE) alternative, or create an SSE app with "
|
|
2079
|
-
"`fastmcp.server.http.create_sse_app` and run it directly.",
|
|
2080
|
-
DeprecationWarning,
|
|
2081
|
-
stacklevel=2,
|
|
2082
|
-
)
|
|
2083
|
-
await self.run_http_async(
|
|
2084
|
-
transport="sse",
|
|
2085
|
-
host=host,
|
|
2086
|
-
port=port,
|
|
2087
|
-
log_level=log_level,
|
|
2088
|
-
path=path,
|
|
2089
|
-
uvicorn_config=uvicorn_config,
|
|
2090
|
-
)
|
|
2091
|
-
|
|
2092
|
-
def sse_app(
|
|
2093
|
-
self,
|
|
2094
|
-
path: str | None = None,
|
|
2095
|
-
message_path: str | None = None,
|
|
2096
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
2097
|
-
) -> StarletteWithLifespan:
|
|
2098
|
-
"""
|
|
2099
|
-
Create a Starlette app for the SSE server.
|
|
2100
|
-
|
|
2101
|
-
Args:
|
|
2102
|
-
path: The path to the SSE endpoint
|
|
2103
|
-
message_path: The path to the message endpoint
|
|
2104
|
-
middleware: A list of middleware to apply to the app
|
|
2105
|
-
"""
|
|
2106
|
-
# Deprecated since 2.3.2
|
|
2107
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2108
|
-
warnings.warn(
|
|
2109
|
-
"The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
|
|
2110
|
-
"alternative, or call `fastmcp.server.http.create_sse_app` directly.",
|
|
2111
|
-
DeprecationWarning,
|
|
2112
|
-
stacklevel=2,
|
|
2113
|
-
)
|
|
2114
|
-
return create_sse_app(
|
|
2115
|
-
server=self,
|
|
2116
|
-
message_path=message_path or self._deprecated_settings.message_path,
|
|
2117
|
-
sse_path=path or self._deprecated_settings.sse_path,
|
|
2118
|
-
auth=self.auth,
|
|
2119
|
-
debug=self._deprecated_settings.debug,
|
|
2120
|
-
middleware=middleware,
|
|
2121
|
-
)
|
|
2122
|
-
|
|
2123
|
-
def streamable_http_app(
|
|
2124
|
-
self,
|
|
2125
|
-
path: str | None = None,
|
|
2126
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
2127
|
-
) -> StarletteWithLifespan:
|
|
2128
|
-
"""
|
|
2129
|
-
Create a Starlette app for the StreamableHTTP server.
|
|
2130
|
-
|
|
2131
|
-
Args:
|
|
2132
|
-
path: The path to the StreamableHTTP endpoint
|
|
2133
|
-
middleware: A list of middleware to apply to the app
|
|
2134
|
-
"""
|
|
2135
|
-
# Deprecated since 2.3.2
|
|
2136
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2137
|
-
warnings.warn(
|
|
2138
|
-
"The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
|
|
2139
|
-
DeprecationWarning,
|
|
2140
|
-
stacklevel=2,
|
|
2141
|
-
)
|
|
2142
|
-
return self.http_app(path=path, middleware=middleware)
|
|
2143
|
-
|
|
2144
2588
|
def http_app(
|
|
2145
2589
|
self,
|
|
2146
2590
|
path: str | None = None,
|
|
@@ -2148,13 +2592,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2148
2592
|
json_response: bool | None = None,
|
|
2149
2593
|
stateless_http: bool | None = None,
|
|
2150
2594
|
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
2595
|
+
event_store: EventStore | None = None,
|
|
2596
|
+
retry_interval: int | None = None,
|
|
2151
2597
|
) -> StarletteWithLifespan:
|
|
2152
2598
|
"""Create a Starlette app using the specified HTTP transport.
|
|
2153
2599
|
|
|
2154
2600
|
Args:
|
|
2155
2601
|
path: The path for the HTTP endpoint
|
|
2156
2602
|
middleware: A list of middleware to apply to the app
|
|
2157
|
-
|
|
2603
|
+
json_response: Whether to use JSON response format
|
|
2604
|
+
stateless_http: Whether to use stateless mode (new transport per request)
|
|
2605
|
+
transport: Transport protocol to use - "http", "streamable-http", or "sse"
|
|
2606
|
+
event_store: Optional event store for SSE polling/resumability. When set,
|
|
2607
|
+
enables clients to reconnect and resume receiving events after
|
|
2608
|
+
server-initiated disconnections. Only used with streamable-http transport.
|
|
2609
|
+
retry_interval: Optional retry interval in milliseconds for SSE polling.
|
|
2610
|
+
Controls how quickly clients should reconnect after server-initiated
|
|
2611
|
+
disconnections. Requires event_store to be set. Only used with
|
|
2612
|
+
streamable-http transport.
|
|
2158
2613
|
|
|
2159
2614
|
Returns:
|
|
2160
2615
|
A Starlette application configured with the specified transport
|
|
@@ -2165,7 +2620,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2165
2620
|
server=self,
|
|
2166
2621
|
streamable_http_path=path
|
|
2167
2622
|
or self._deprecated_settings.streamable_http_path,
|
|
2168
|
-
event_store=
|
|
2623
|
+
event_store=event_store,
|
|
2624
|
+
retry_interval=retry_interval,
|
|
2169
2625
|
auth=self.auth,
|
|
2170
2626
|
json_response=(
|
|
2171
2627
|
json_response
|
|
@@ -2190,40 +2646,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2190
2646
|
middleware=middleware,
|
|
2191
2647
|
)
|
|
2192
2648
|
|
|
2193
|
-
async def run_streamable_http_async(
|
|
2194
|
-
self,
|
|
2195
|
-
host: str | None = None,
|
|
2196
|
-
port: int | None = None,
|
|
2197
|
-
log_level: str | None = None,
|
|
2198
|
-
path: str | None = None,
|
|
2199
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
2200
|
-
) -> None:
|
|
2201
|
-
# Deprecated since 2.3.2
|
|
2202
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2203
|
-
warnings.warn(
|
|
2204
|
-
"The run_streamable_http_async method is deprecated (as of 2.3.2). "
|
|
2205
|
-
"Use run_http_async instead.",
|
|
2206
|
-
DeprecationWarning,
|
|
2207
|
-
stacklevel=2,
|
|
2208
|
-
)
|
|
2209
|
-
await self.run_http_async(
|
|
2210
|
-
transport="http",
|
|
2211
|
-
host=host,
|
|
2212
|
-
port=port,
|
|
2213
|
-
log_level=log_level,
|
|
2214
|
-
path=path,
|
|
2215
|
-
uvicorn_config=uvicorn_config,
|
|
2216
|
-
)
|
|
2217
|
-
|
|
2218
2649
|
def mount(
|
|
2219
2650
|
self,
|
|
2220
2651
|
server: FastMCP[LifespanResultT],
|
|
2221
2652
|
prefix: str | None = None,
|
|
2222
2653
|
as_proxy: bool | None = None,
|
|
2223
|
-
*,
|
|
2224
|
-
tool_separator: str | None = None,
|
|
2225
|
-
resource_separator: str | None = None,
|
|
2226
|
-
prompt_separator: str | None = None,
|
|
2227
2654
|
) -> None:
|
|
2228
2655
|
"""Mount another FastMCP server on this server with an optional prefix.
|
|
2229
2656
|
|
|
@@ -2268,56 +2695,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2268
2695
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
2269
2696
|
automatically determined based on whether the server has a custom lifespan
|
|
2270
2697
|
(True if it has a custom lifespan, False otherwise).
|
|
2271
|
-
tool_separator: Deprecated. Separator character for tool names.
|
|
2272
|
-
resource_separator: Deprecated. Separator character for resource URIs.
|
|
2273
|
-
prompt_separator: Deprecated. Separator character for prompt names.
|
|
2274
2698
|
"""
|
|
2275
2699
|
from fastmcp.server.proxy import FastMCPProxy
|
|
2276
2700
|
|
|
2277
|
-
# Deprecated since 2.9.0
|
|
2278
|
-
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
2279
|
-
# second was the server. Here we swap them if needed now that the prefix
|
|
2280
|
-
# is optional.
|
|
2281
|
-
if isinstance(server, str):
|
|
2282
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2283
|
-
warnings.warn(
|
|
2284
|
-
"Mount prefixes are now optional and the first positional argument "
|
|
2285
|
-
"should be the server you want to mount.",
|
|
2286
|
-
DeprecationWarning,
|
|
2287
|
-
stacklevel=2,
|
|
2288
|
-
)
|
|
2289
|
-
server, prefix = cast(FastMCP[Any], prefix), server
|
|
2290
|
-
|
|
2291
|
-
if tool_separator is not None:
|
|
2292
|
-
# Deprecated since 2.4.0
|
|
2293
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2294
|
-
warnings.warn(
|
|
2295
|
-
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
2296
|
-
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
2297
|
-
DeprecationWarning,
|
|
2298
|
-
stacklevel=2,
|
|
2299
|
-
)
|
|
2300
|
-
|
|
2301
|
-
if resource_separator is not None:
|
|
2302
|
-
# Deprecated since 2.4.0
|
|
2303
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2304
|
-
warnings.warn(
|
|
2305
|
-
"The resource_separator parameter is deprecated and ignored. "
|
|
2306
|
-
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
2307
|
-
DeprecationWarning,
|
|
2308
|
-
stacklevel=2,
|
|
2309
|
-
)
|
|
2310
|
-
|
|
2311
|
-
if prompt_separator is not None:
|
|
2312
|
-
# Deprecated since 2.4.0
|
|
2313
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2314
|
-
warnings.warn(
|
|
2315
|
-
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
2316
|
-
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
2317
|
-
DeprecationWarning,
|
|
2318
|
-
stacklevel=2,
|
|
2319
|
-
)
|
|
2320
|
-
|
|
2321
2701
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
2322
2702
|
# we should treat it as a proxy
|
|
2323
2703
|
if as_proxy is None:
|
|
@@ -2326,11 +2706,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2326
2706
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
2327
2707
|
server = FastMCP.as_proxy(server)
|
|
2328
2708
|
|
|
2709
|
+
# Mark the server as mounted so it skips creating its own Docket/Worker.
|
|
2710
|
+
# The parent's Docket handles task execution, avoiding race conditions
|
|
2711
|
+
# with multiple workers competing for tasks from the same queue.
|
|
2712
|
+
server._is_mounted = True
|
|
2713
|
+
|
|
2329
2714
|
# Delegate mounting to all three managers
|
|
2330
2715
|
mounted_server = MountedServer(
|
|
2331
2716
|
prefix=prefix,
|
|
2332
2717
|
server=server,
|
|
2333
|
-
resource_prefix_format=self.resource_prefix_format,
|
|
2334
2718
|
)
|
|
2335
2719
|
self._mounted_servers.append(mounted_server)
|
|
2336
2720
|
|
|
@@ -2338,9 +2722,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2338
2722
|
self,
|
|
2339
2723
|
server: FastMCP[LifespanResultT],
|
|
2340
2724
|
prefix: str | None = None,
|
|
2341
|
-
tool_separator: str | None = None,
|
|
2342
|
-
resource_separator: str | None = None,
|
|
2343
|
-
prompt_separator: str | None = None,
|
|
2344
2725
|
) -> None:
|
|
2345
2726
|
"""
|
|
2346
2727
|
Import the MCP objects from another FastMCP server into this one,
|
|
@@ -2372,56 +2753,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2372
2753
|
server: The FastMCP server to import
|
|
2373
2754
|
prefix: Optional prefix to use for the imported server's objects. If None,
|
|
2374
2755
|
objects are imported with their original names.
|
|
2375
|
-
|
|
2376
|
-
resource_separator: Deprecated and ignored. Prefix is now
|
|
2377
|
-
applied using the protocol://prefix/path format
|
|
2378
|
-
prompt_separator: Deprecated. Separator for prompt names.
|
|
2379
|
-
"""
|
|
2380
|
-
|
|
2381
|
-
# Deprecated since 2.9.0
|
|
2382
|
-
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
2383
|
-
# second was the server. Here we swap them if needed now that the prefix
|
|
2384
|
-
# is optional.
|
|
2385
|
-
if isinstance(server, str):
|
|
2386
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2387
|
-
warnings.warn(
|
|
2388
|
-
"Import prefixes are now optional and the first positional argument "
|
|
2389
|
-
"should be the server you want to import.",
|
|
2390
|
-
DeprecationWarning,
|
|
2391
|
-
stacklevel=2,
|
|
2392
|
-
)
|
|
2393
|
-
server, prefix = cast(FastMCP[Any], prefix), server
|
|
2394
|
-
|
|
2395
|
-
if tool_separator is not None:
|
|
2396
|
-
# Deprecated since 2.4.0
|
|
2397
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2398
|
-
warnings.warn(
|
|
2399
|
-
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
2400
|
-
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
2401
|
-
DeprecationWarning,
|
|
2402
|
-
stacklevel=2,
|
|
2403
|
-
)
|
|
2404
|
-
|
|
2405
|
-
if resource_separator is not None:
|
|
2406
|
-
# Deprecated since 2.4.0
|
|
2407
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2408
|
-
warnings.warn(
|
|
2409
|
-
"The resource_separator parameter is deprecated and ignored. "
|
|
2410
|
-
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
2411
|
-
DeprecationWarning,
|
|
2412
|
-
stacklevel=2,
|
|
2413
|
-
)
|
|
2414
|
-
|
|
2415
|
-
if prompt_separator is not None:
|
|
2416
|
-
# Deprecated since 2.4.0
|
|
2417
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2418
|
-
warnings.warn(
|
|
2419
|
-
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
2420
|
-
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
2421
|
-
DeprecationWarning,
|
|
2422
|
-
stacklevel=2,
|
|
2423
|
-
)
|
|
2424
|
-
|
|
2756
|
+
"""
|
|
2425
2757
|
# Import tools from the server
|
|
2426
2758
|
for key, tool in (await server.get_tools()).items():
|
|
2427
2759
|
if prefix:
|
|
@@ -2431,9 +2763,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2431
2763
|
# Import resources and templates from the server
|
|
2432
2764
|
for key, resource in (await server.get_resources()).items():
|
|
2433
2765
|
if prefix:
|
|
2434
|
-
resource_key = add_resource_prefix(
|
|
2435
|
-
key, prefix, self.resource_prefix_format
|
|
2436
|
-
)
|
|
2766
|
+
resource_key = add_resource_prefix(key, prefix)
|
|
2437
2767
|
resource = resource.model_copy(
|
|
2438
2768
|
update={"name": f"{prefix}_{resource.name}"}, key=resource_key
|
|
2439
2769
|
)
|
|
@@ -2441,9 +2771,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2441
2771
|
|
|
2442
2772
|
for key, template in (await server.get_resource_templates()).items():
|
|
2443
2773
|
if prefix:
|
|
2444
|
-
template_key = add_resource_prefix(
|
|
2445
|
-
key, prefix, self.resource_prefix_format
|
|
2446
|
-
)
|
|
2774
|
+
template_key = add_resource_prefix(key, prefix)
|
|
2447
2775
|
template = template.model_copy(
|
|
2448
2776
|
update={"name": f"{prefix}_{template.name}"}, key=template_key
|
|
2449
2777
|
)
|
|
@@ -2476,66 +2804,46 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2476
2804
|
cls,
|
|
2477
2805
|
openapi_spec: dict[str, Any],
|
|
2478
2806
|
client: httpx.AsyncClient,
|
|
2479
|
-
route_maps: list[RouteMap] |
|
|
2480
|
-
route_map_fn: OpenAPIRouteMapFn |
|
|
2481
|
-
mcp_component_fn: OpenAPIComponentFn |
|
|
2807
|
+
route_maps: list[RouteMap] | None = None,
|
|
2808
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2809
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
2482
2810
|
mcp_names: dict[str, str] | None = None,
|
|
2483
2811
|
tags: set[str] | None = None,
|
|
2484
2812
|
**settings: Any,
|
|
2485
|
-
) -> FastMCPOpenAPI
|
|
2813
|
+
) -> FastMCPOpenAPI:
|
|
2486
2814
|
"""
|
|
2487
2815
|
Create a FastMCP server from an OpenAPI specification.
|
|
2488
2816
|
"""
|
|
2817
|
+
from .openapi import FastMCPOpenAPI
|
|
2489
2818
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
mcp_names=mcp_names,
|
|
2501
|
-
tags=tags,
|
|
2502
|
-
**settings,
|
|
2503
|
-
)
|
|
2504
|
-
else:
|
|
2505
|
-
logger.info(
|
|
2506
|
-
"Using legacy OpenAPI parser. To use the new parser, set "
|
|
2507
|
-
"FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
|
|
2508
|
-
"was introduced for testing in 2.11 and will become the default soon."
|
|
2509
|
-
)
|
|
2510
|
-
from .openapi import FastMCPOpenAPI
|
|
2511
|
-
|
|
2512
|
-
return FastMCPOpenAPI(
|
|
2513
|
-
openapi_spec=openapi_spec,
|
|
2514
|
-
client=client,
|
|
2515
|
-
route_maps=cast(Any, route_maps),
|
|
2516
|
-
route_map_fn=cast(Any, route_map_fn),
|
|
2517
|
-
mcp_component_fn=cast(Any, mcp_component_fn),
|
|
2518
|
-
mcp_names=mcp_names,
|
|
2519
|
-
tags=tags,
|
|
2520
|
-
**settings,
|
|
2521
|
-
)
|
|
2819
|
+
return FastMCPOpenAPI(
|
|
2820
|
+
openapi_spec=openapi_spec,
|
|
2821
|
+
client=client,
|
|
2822
|
+
route_maps=route_maps,
|
|
2823
|
+
route_map_fn=route_map_fn,
|
|
2824
|
+
mcp_component_fn=mcp_component_fn,
|
|
2825
|
+
mcp_names=mcp_names,
|
|
2826
|
+
tags=tags,
|
|
2827
|
+
**settings,
|
|
2828
|
+
)
|
|
2522
2829
|
|
|
2523
2830
|
@classmethod
|
|
2524
2831
|
def from_fastapi(
|
|
2525
2832
|
cls,
|
|
2526
2833
|
app: Any,
|
|
2527
2834
|
name: str | None = None,
|
|
2528
|
-
route_maps: list[RouteMap] |
|
|
2529
|
-
route_map_fn: OpenAPIRouteMapFn |
|
|
2530
|
-
mcp_component_fn: OpenAPIComponentFn |
|
|
2835
|
+
route_maps: list[RouteMap] | None = None,
|
|
2836
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2837
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
2531
2838
|
mcp_names: dict[str, str] | None = None,
|
|
2532
2839
|
httpx_client_kwargs: dict[str, Any] | None = None,
|
|
2533
2840
|
tags: set[str] | None = None,
|
|
2534
2841
|
**settings: Any,
|
|
2535
|
-
) -> FastMCPOpenAPI
|
|
2842
|
+
) -> FastMCPOpenAPI:
|
|
2536
2843
|
"""
|
|
2537
2844
|
Create a FastMCP server from a FastAPI application.
|
|
2538
2845
|
"""
|
|
2846
|
+
from .openapi import FastMCPOpenAPI
|
|
2539
2847
|
|
|
2540
2848
|
if httpx_client_kwargs is None:
|
|
2541
2849
|
httpx_client_kwargs = {}
|
|
@@ -2548,40 +2856,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2548
2856
|
|
|
2549
2857
|
name = name or app.title
|
|
2550
2858
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
mcp_names=mcp_names,
|
|
2563
|
-
tags=tags,
|
|
2564
|
-
**settings,
|
|
2565
|
-
)
|
|
2566
|
-
else:
|
|
2567
|
-
logger.info(
|
|
2568
|
-
"Using legacy OpenAPI parser. To use the new parser, set "
|
|
2569
|
-
"FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
|
|
2570
|
-
"was introduced for testing in 2.11 and will become the default soon."
|
|
2571
|
-
)
|
|
2572
|
-
from .openapi import FastMCPOpenAPI
|
|
2573
|
-
|
|
2574
|
-
return FastMCPOpenAPI(
|
|
2575
|
-
openapi_spec=app.openapi(),
|
|
2576
|
-
client=client,
|
|
2577
|
-
name=name,
|
|
2578
|
-
route_maps=cast(Any, route_maps),
|
|
2579
|
-
route_map_fn=cast(Any, route_map_fn),
|
|
2580
|
-
mcp_component_fn=cast(Any, mcp_component_fn),
|
|
2581
|
-
mcp_names=mcp_names,
|
|
2582
|
-
tags=tags,
|
|
2583
|
-
**settings,
|
|
2584
|
-
)
|
|
2859
|
+
return FastMCPOpenAPI(
|
|
2860
|
+
openapi_spec=app.openapi(),
|
|
2861
|
+
client=client,
|
|
2862
|
+
name=name,
|
|
2863
|
+
route_maps=route_maps,
|
|
2864
|
+
route_map_fn=route_map_fn,
|
|
2865
|
+
mcp_component_fn=mcp_component_fn,
|
|
2866
|
+
mcp_names=mcp_names,
|
|
2867
|
+
tags=tags,
|
|
2868
|
+
**settings,
|
|
2869
|
+
)
|
|
2585
2870
|
|
|
2586
2871
|
@classmethod
|
|
2587
2872
|
def as_proxy(
|
|
@@ -2643,23 +2928,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2643
2928
|
|
|
2644
2929
|
return FastMCPProxy(client_factory=client_factory, **settings)
|
|
2645
2930
|
|
|
2646
|
-
@classmethod
|
|
2647
|
-
def from_client(
|
|
2648
|
-
cls, client: Client[ClientTransportT], **settings: Any
|
|
2649
|
-
) -> FastMCPProxy:
|
|
2650
|
-
"""
|
|
2651
|
-
Create a FastMCP proxy server from a FastMCP client.
|
|
2652
|
-
"""
|
|
2653
|
-
# Deprecated since 2.3.5
|
|
2654
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2655
|
-
warnings.warn(
|
|
2656
|
-
"FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
|
|
2657
|
-
DeprecationWarning,
|
|
2658
|
-
stacklevel=2,
|
|
2659
|
-
)
|
|
2660
|
-
|
|
2661
|
-
return cls.as_proxy(client, **settings)
|
|
2662
|
-
|
|
2663
2931
|
def _should_enable_component(
|
|
2664
2932
|
self,
|
|
2665
2933
|
component: FastMCPComponent,
|
|
@@ -2706,13 +2974,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2706
2974
|
class MountedServer:
|
|
2707
2975
|
prefix: str | None
|
|
2708
2976
|
server: FastMCP[Any]
|
|
2709
|
-
resource_prefix_format: Literal["protocol", "path"] | None = None
|
|
2710
2977
|
|
|
2711
2978
|
|
|
2712
|
-
def add_resource_prefix(
|
|
2713
|
-
|
|
2714
|
-
) -> str:
|
|
2715
|
-
"""Add a prefix to a resource URI.
|
|
2979
|
+
def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
2980
|
+
"""Add a prefix to a resource URI using path formatting (resource://prefix/path).
|
|
2716
2981
|
|
|
2717
2982
|
Args:
|
|
2718
2983
|
uri: The original resource URI
|
|
@@ -2722,16 +2987,10 @@ def add_resource_prefix(
|
|
|
2722
2987
|
The resource URI with the prefix added
|
|
2723
2988
|
|
|
2724
2989
|
Examples:
|
|
2725
|
-
With new style:
|
|
2726
2990
|
```python
|
|
2727
2991
|
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2728
2992
|
"resource://prefix/path/to/resource"
|
|
2729
2993
|
```
|
|
2730
|
-
With legacy style:
|
|
2731
|
-
```python
|
|
2732
|
-
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2733
|
-
"prefix+resource://path/to/resource"
|
|
2734
|
-
```
|
|
2735
2994
|
With absolute path:
|
|
2736
2995
|
```python
|
|
2737
2996
|
add_resource_prefix("resource:///absolute/path", "prefix")
|
|
@@ -2744,54 +3003,32 @@ def add_resource_prefix(
|
|
|
2744
3003
|
if not prefix:
|
|
2745
3004
|
return uri
|
|
2746
3005
|
|
|
2747
|
-
#
|
|
3006
|
+
# Split the URI into protocol and path
|
|
3007
|
+
match = URI_PATTERN.match(uri)
|
|
3008
|
+
if not match:
|
|
3009
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2748
3010
|
|
|
2749
|
-
|
|
2750
|
-
prefix_format = fastmcp.settings.resource_prefix_format
|
|
3011
|
+
protocol, path = match.groups()
|
|
2751
3012
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
return f"{prefix}+{uri}"
|
|
2755
|
-
elif prefix_format == "path":
|
|
2756
|
-
# New style: protocol://prefix/path
|
|
2757
|
-
# Split the URI into protocol and path
|
|
2758
|
-
match = URI_PATTERN.match(uri)
|
|
2759
|
-
if not match:
|
|
2760
|
-
raise ValueError(
|
|
2761
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2762
|
-
)
|
|
3013
|
+
# Add the prefix to the path
|
|
3014
|
+
return f"{protocol}{prefix}/{path}"
|
|
2763
3015
|
|
|
2764
|
-
protocol, path = match.groups()
|
|
2765
3016
|
|
|
2766
|
-
|
|
2767
|
-
return f"{protocol}{prefix}/{path}"
|
|
2768
|
-
else:
|
|
2769
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
def remove_resource_prefix(
|
|
2773
|
-
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
2774
|
-
) -> str:
|
|
3017
|
+
def remove_resource_prefix(uri: str, prefix: str) -> str:
|
|
2775
3018
|
"""Remove a prefix from a resource URI.
|
|
2776
3019
|
|
|
2777
3020
|
Args:
|
|
2778
3021
|
uri: The resource URI with a prefix
|
|
2779
3022
|
prefix: The prefix to remove
|
|
2780
|
-
|
|
3023
|
+
|
|
2781
3024
|
Returns:
|
|
2782
3025
|
The resource URI with the prefix removed
|
|
2783
3026
|
|
|
2784
3027
|
Examples:
|
|
2785
|
-
With new style:
|
|
2786
3028
|
```python
|
|
2787
3029
|
remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2788
3030
|
"resource://path/to/resource"
|
|
2789
3031
|
```
|
|
2790
|
-
With legacy style:
|
|
2791
|
-
```python
|
|
2792
|
-
remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2793
|
-
"resource://path/to/resource"
|
|
2794
|
-
```
|
|
2795
3032
|
With absolute path:
|
|
2796
3033
|
```python
|
|
2797
3034
|
remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
@@ -2804,41 +3041,24 @@ def remove_resource_prefix(
|
|
|
2804
3041
|
if not prefix:
|
|
2805
3042
|
return uri
|
|
2806
3043
|
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
# Legacy style: prefix+protocol://path
|
|
2812
|
-
legacy_prefix = f"{prefix}+"
|
|
2813
|
-
if uri.startswith(legacy_prefix):
|
|
2814
|
-
return uri[len(legacy_prefix) :]
|
|
2815
|
-
return uri
|
|
2816
|
-
elif prefix_format == "path":
|
|
2817
|
-
# New style: protocol://prefix/path
|
|
2818
|
-
# Split the URI into protocol and path
|
|
2819
|
-
match = URI_PATTERN.match(uri)
|
|
2820
|
-
if not match:
|
|
2821
|
-
raise ValueError(
|
|
2822
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2823
|
-
)
|
|
3044
|
+
# Split the URI into protocol and path
|
|
3045
|
+
match = URI_PATTERN.match(uri)
|
|
3046
|
+
if not match:
|
|
3047
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2824
3048
|
|
|
2825
|
-
|
|
3049
|
+
protocol, path = match.groups()
|
|
2826
3050
|
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
3051
|
+
# Check if the path starts with the prefix followed by a /
|
|
3052
|
+
prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
|
|
3053
|
+
path_match = re.match(prefix_pattern, path)
|
|
3054
|
+
if not path_match:
|
|
3055
|
+
return uri
|
|
2832
3056
|
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
else:
|
|
2836
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3057
|
+
# Return the URI without the prefix
|
|
3058
|
+
return f"{protocol}{path_match.group(1)}"
|
|
2837
3059
|
|
|
2838
3060
|
|
|
2839
|
-
def has_resource_prefix(
|
|
2840
|
-
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
2841
|
-
) -> bool:
|
|
3061
|
+
def has_resource_prefix(uri: str, prefix: str) -> bool:
|
|
2842
3062
|
"""Check if a resource URI has a specific prefix.
|
|
2843
3063
|
|
|
2844
3064
|
Args:
|
|
@@ -2849,16 +3069,10 @@ def has_resource_prefix(
|
|
|
2849
3069
|
True if the URI has the specified prefix, False otherwise
|
|
2850
3070
|
|
|
2851
3071
|
Examples:
|
|
2852
|
-
With new style:
|
|
2853
3072
|
```python
|
|
2854
3073
|
has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2855
3074
|
True
|
|
2856
3075
|
```
|
|
2857
|
-
With legacy style:
|
|
2858
|
-
```python
|
|
2859
|
-
has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2860
|
-
True
|
|
2861
|
-
```
|
|
2862
3076
|
With other path:
|
|
2863
3077
|
```python
|
|
2864
3078
|
has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
@@ -2871,28 +3085,13 @@ def has_resource_prefix(
|
|
|
2871
3085
|
if not prefix:
|
|
2872
3086
|
return False
|
|
2873
3087
|
|
|
2874
|
-
#
|
|
2875
|
-
|
|
2876
|
-
if
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
if prefix_format == "protocol":
|
|
2880
|
-
# Legacy style: prefix+protocol://path
|
|
2881
|
-
legacy_prefix = f"{prefix}+"
|
|
2882
|
-
return uri.startswith(legacy_prefix)
|
|
2883
|
-
elif prefix_format == "path":
|
|
2884
|
-
# New style: protocol://prefix/path
|
|
2885
|
-
# Split the URI into protocol and path
|
|
2886
|
-
match = URI_PATTERN.match(uri)
|
|
2887
|
-
if not match:
|
|
2888
|
-
raise ValueError(
|
|
2889
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2890
|
-
)
|
|
3088
|
+
# Split the URI into protocol and path
|
|
3089
|
+
match = URI_PATTERN.match(uri)
|
|
3090
|
+
if not match:
|
|
3091
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2891
3092
|
|
|
2892
|
-
|
|
3093
|
+
_, path = match.groups()
|
|
2893
3094
|
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
else:
|
|
2898
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3095
|
+
# Check if the path starts with the prefix followed by a /
|
|
3096
|
+
prefix_pattern = f"^{re.escape(prefix)}/"
|
|
3097
|
+
return bool(re.match(prefix_pattern, path))
|