fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import inspect
|
|
7
6
|
import re
|
|
8
7
|
import secrets
|
|
9
8
|
import warnings
|
|
10
|
-
import weakref
|
|
11
9
|
from collections.abc import (
|
|
12
10
|
AsyncIterator,
|
|
13
11
|
Awaitable,
|
|
@@ -18,103 +16,155 @@ from collections.abc import (
|
|
|
18
16
|
)
|
|
19
17
|
from contextlib import (
|
|
20
18
|
AbstractAsyncContextManager,
|
|
21
|
-
AsyncExitStack,
|
|
22
19
|
asynccontextmanager,
|
|
23
|
-
suppress,
|
|
24
20
|
)
|
|
25
|
-
from dataclasses import
|
|
21
|
+
from dataclasses import replace
|
|
26
22
|
from functools import partial
|
|
27
23
|
from pathlib import Path
|
|
28
24
|
from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
|
|
29
25
|
|
|
30
|
-
import anyio
|
|
31
26
|
import httpx
|
|
32
27
|
import mcp.types
|
|
33
|
-
import
|
|
34
|
-
from
|
|
35
|
-
from
|
|
36
|
-
from mcp.server.lowlevel.server import LifespanResultT
|
|
37
|
-
from mcp.server.stdio import stdio_server
|
|
28
|
+
from key_value.aio.adapters.pydantic import PydanticAdapter
|
|
29
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
30
|
+
from key_value.aio.stores.memory import MemoryStore
|
|
31
|
+
from mcp.server.lowlevel.server import LifespanResultT
|
|
38
32
|
from mcp.shared.exceptions import McpError
|
|
39
33
|
from mcp.types import (
|
|
40
|
-
METHOD_NOT_FOUND,
|
|
41
34
|
Annotations,
|
|
42
35
|
AnyFunction,
|
|
43
36
|
CallToolRequestParams,
|
|
44
|
-
ContentBlock,
|
|
45
|
-
ErrorData,
|
|
46
|
-
GetPromptResult,
|
|
47
37
|
ToolAnnotations,
|
|
48
38
|
)
|
|
49
|
-
from mcp.types import Prompt as SDKPrompt
|
|
50
|
-
from mcp.types import Resource as SDKResource
|
|
51
|
-
from mcp.types import ResourceTemplate as SDKResourceTemplate
|
|
52
|
-
from mcp.types import Tool as SDKTool
|
|
53
39
|
from pydantic import AnyUrl
|
|
54
|
-
from
|
|
55
|
-
from starlette.
|
|
56
|
-
from
|
|
57
|
-
from starlette.routing import BaseRoute, Route
|
|
40
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
41
|
+
from starlette.routing import BaseRoute
|
|
42
|
+
from typing_extensions import Self
|
|
58
43
|
|
|
59
44
|
import fastmcp
|
|
60
45
|
import fastmcp.server
|
|
61
|
-
from fastmcp.exceptions import
|
|
46
|
+
from fastmcp.exceptions import (
|
|
47
|
+
AuthorizationError,
|
|
48
|
+
FastMCPError,
|
|
49
|
+
NotFoundError,
|
|
50
|
+
PromptError,
|
|
51
|
+
ResourceError,
|
|
52
|
+
ToolError,
|
|
53
|
+
ValidationError,
|
|
54
|
+
)
|
|
62
55
|
from fastmcp.mcp_config import MCPConfig
|
|
63
56
|
from fastmcp.prompts import Prompt
|
|
64
|
-
from fastmcp.prompts.
|
|
65
|
-
from fastmcp.prompts.
|
|
66
|
-
from fastmcp.resources.resource import
|
|
67
|
-
from fastmcp.resources.
|
|
68
|
-
from fastmcp.
|
|
69
|
-
from fastmcp.server.
|
|
70
|
-
from fastmcp.server.
|
|
71
|
-
from fastmcp.server.http import (
|
|
72
|
-
StarletteWithLifespan,
|
|
73
|
-
create_sse_app,
|
|
74
|
-
create_streamable_http_app,
|
|
75
|
-
)
|
|
57
|
+
from fastmcp.prompts.function_prompt import FunctionPrompt
|
|
58
|
+
from fastmcp.prompts.prompt import PromptResult
|
|
59
|
+
from fastmcp.resources.resource import Resource, ResourceResult
|
|
60
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
61
|
+
from fastmcp.server.auth import AuthContext, AuthProvider, run_auth_checks
|
|
62
|
+
from fastmcp.server.dependencies import get_access_token
|
|
63
|
+
from fastmcp.server.lifespan import Lifespan
|
|
76
64
|
from fastmcp.server.low_level import LowLevelServer
|
|
77
65
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
78
|
-
from fastmcp.server.
|
|
79
|
-
from fastmcp.server.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
from fastmcp.server.mixins import LifespanMixin, MCPOperationsMixin, TransportMixin
|
|
67
|
+
from fastmcp.server.providers import LocalProvider, Provider
|
|
68
|
+
from fastmcp.server.providers.aggregate import AggregateProvider
|
|
69
|
+
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
|
70
|
+
from fastmcp.server.telemetry import server_span
|
|
71
|
+
from fastmcp.server.transforms import (
|
|
72
|
+
ToolTransform,
|
|
73
|
+
Transform,
|
|
83
74
|
)
|
|
75
|
+
from fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled
|
|
76
|
+
from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting
|
|
84
77
|
from fastmcp.settings import Settings
|
|
85
|
-
from fastmcp.tools.
|
|
86
|
-
from fastmcp.tools.
|
|
78
|
+
from fastmcp.tools.function_tool import FunctionTool
|
|
79
|
+
from fastmcp.tools.tool import AuthCheckCallable, Tool, ToolResult
|
|
87
80
|
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
88
|
-
from fastmcp.utilities.cli import log_server_banner
|
|
89
81
|
from fastmcp.utilities.components import FastMCPComponent
|
|
90
|
-
from fastmcp.utilities.logging import get_logger
|
|
91
|
-
from fastmcp.utilities.types import NotSet, NotSetT
|
|
82
|
+
from fastmcp.utilities.logging import get_logger
|
|
83
|
+
from fastmcp.utilities.types import FastMCPBaseModel, NotSet, NotSetT
|
|
84
|
+
from fastmcp.utilities.versions import (
|
|
85
|
+
VersionSpec,
|
|
86
|
+
)
|
|
92
87
|
|
|
93
88
|
if TYPE_CHECKING:
|
|
94
89
|
from fastmcp.client import Client
|
|
95
90
|
from fastmcp.client.client import FastMCP1Server
|
|
96
91
|
from fastmcp.client.sampling import SamplingHandler
|
|
97
92
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
98
|
-
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
99
|
-
from fastmcp.server.openapi import
|
|
100
|
-
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
101
|
-
from fastmcp.server.proxy import FastMCPProxy
|
|
93
|
+
from fastmcp.server.providers.openapi import ComponentFn as OpenAPIComponentFn
|
|
94
|
+
from fastmcp.server.providers.openapi import RouteMap
|
|
95
|
+
from fastmcp.server.providers.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
96
|
+
from fastmcp.server.providers.proxy import FastMCPProxy
|
|
102
97
|
from fastmcp.tools.tool import ToolResultSerializerType
|
|
103
98
|
|
|
104
99
|
logger = get_logger(__name__)
|
|
105
100
|
|
|
106
101
|
|
|
107
102
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_on_duplicate(
|
|
106
|
+
on_duplicate: DuplicateBehavior | None,
|
|
107
|
+
on_duplicate_tools: DuplicateBehavior | None,
|
|
108
|
+
on_duplicate_resources: DuplicateBehavior | None,
|
|
109
|
+
on_duplicate_prompts: DuplicateBehavior | None,
|
|
110
|
+
) -> DuplicateBehavior:
|
|
111
|
+
"""Resolve on_duplicate from deprecated per-type params.
|
|
112
|
+
|
|
113
|
+
Takes the most strict value if multiple are provided.
|
|
114
|
+
Delete this function when removing deprecated params.
|
|
115
|
+
"""
|
|
116
|
+
strictness_order: list[DuplicateBehavior] = ["error", "warn", "replace", "ignore"]
|
|
117
|
+
deprecated_values: list[DuplicateBehavior] = []
|
|
118
|
+
|
|
119
|
+
deprecated_params: list[tuple[str, DuplicateBehavior | None]] = [
|
|
120
|
+
("on_duplicate_tools", on_duplicate_tools),
|
|
121
|
+
("on_duplicate_resources", on_duplicate_resources),
|
|
122
|
+
("on_duplicate_prompts", on_duplicate_prompts),
|
|
123
|
+
]
|
|
124
|
+
for name, value in deprecated_params:
|
|
125
|
+
if value is not None:
|
|
126
|
+
if fastmcp.settings.deprecation_warnings:
|
|
127
|
+
warnings.warn(
|
|
128
|
+
f"{name} is deprecated, use on_duplicate instead",
|
|
129
|
+
DeprecationWarning,
|
|
130
|
+
stacklevel=4,
|
|
131
|
+
)
|
|
132
|
+
deprecated_values.append(value)
|
|
133
|
+
|
|
134
|
+
if on_duplicate is None and deprecated_values:
|
|
135
|
+
return min(deprecated_values, key=lambda x: strictness_order.index(x))
|
|
136
|
+
|
|
137
|
+
return on_duplicate or "warn"
|
|
138
|
+
|
|
139
|
+
|
|
108
140
|
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
109
141
|
|
|
110
142
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
111
143
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
112
144
|
|
|
145
|
+
|
|
113
146
|
LifespanCallable = Callable[
|
|
114
147
|
["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
|
|
115
148
|
]
|
|
116
149
|
|
|
117
150
|
|
|
151
|
+
def _get_auth_context() -> tuple[bool, Any]:
|
|
152
|
+
"""Get auth context for the current request.
|
|
153
|
+
|
|
154
|
+
Returns a tuple of (skip_auth, token) where:
|
|
155
|
+
- skip_auth=True means auth checks should be skipped (STDIO transport)
|
|
156
|
+
- token is the access token for HTTP transports (may be None if unauthenticated)
|
|
157
|
+
|
|
158
|
+
Uses late import to avoid circular import with context.py.
|
|
159
|
+
"""
|
|
160
|
+
from fastmcp.server.context import _current_transport
|
|
161
|
+
|
|
162
|
+
is_stdio = _current_transport.get() == "stdio"
|
|
163
|
+
if is_stdio:
|
|
164
|
+
return (True, None)
|
|
165
|
+
return (False, get_access_token())
|
|
166
|
+
|
|
167
|
+
|
|
118
168
|
@asynccontextmanager
|
|
119
169
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
120
170
|
"""Default lifespan context manager that does nothing.
|
|
@@ -152,7 +202,19 @@ def _lifespan_proxy(
|
|
|
152
202
|
return wrap
|
|
153
203
|
|
|
154
204
|
|
|
155
|
-
class
|
|
205
|
+
class StateValue(FastMCPBaseModel):
|
|
206
|
+
"""Wrapper for stored context state values."""
|
|
207
|
+
|
|
208
|
+
value: Any
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class FastMCP(
|
|
212
|
+
AggregateProvider,
|
|
213
|
+
LifespanMixin,
|
|
214
|
+
MCPOperationsMixin,
|
|
215
|
+
TransportMixin,
|
|
216
|
+
Generic[LifespanResultT],
|
|
217
|
+
):
|
|
156
218
|
def __init__(
|
|
157
219
|
self,
|
|
158
220
|
name: str | None = None,
|
|
@@ -161,26 +223,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
161
223
|
version: str | None = None,
|
|
162
224
|
website_url: str | None = None,
|
|
163
225
|
icons: list[mcp.types.Icon] | None = None,
|
|
164
|
-
auth: AuthProvider |
|
|
226
|
+
auth: AuthProvider | None = None,
|
|
165
227
|
middleware: Sequence[Middleware] | None = None,
|
|
166
|
-
|
|
228
|
+
providers: Sequence[Provider] | None = None,
|
|
229
|
+
lifespan: LifespanCallable | Lifespan | None = None,
|
|
167
230
|
mask_error_details: bool | None = None,
|
|
168
231
|
tools: Sequence[Tool | Callable[..., Any]] | None = None,
|
|
169
|
-
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
170
232
|
tool_serializer: ToolResultSerializerType | None = None,
|
|
171
233
|
include_tags: Collection[str] | None = None,
|
|
172
234
|
exclude_tags: Collection[str] | None = None,
|
|
173
|
-
|
|
174
|
-
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
175
|
-
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
176
|
-
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
235
|
+
on_duplicate: DuplicateBehavior | None = None,
|
|
177
236
|
strict_input_validation: bool | None = None,
|
|
237
|
+
list_page_size: int | None = None,
|
|
178
238
|
tasks: bool | None = None,
|
|
239
|
+
session_state_store: AsyncKeyValue | None = None,
|
|
179
240
|
# ---
|
|
241
|
+
# --- DEPRECATED parameters ---
|
|
180
242
|
# ---
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
243
|
+
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
244
|
+
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
245
|
+
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
184
246
|
log_level: str | None = None,
|
|
185
247
|
debug: bool | None = None,
|
|
186
248
|
host: str | None = None,
|
|
@@ -192,7 +254,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
192
254
|
stateless_http: bool | None = None,
|
|
193
255
|
sampling_handler: SamplingHandler | None = None,
|
|
194
256
|
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
257
|
+
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
195
258
|
):
|
|
259
|
+
# Initialize Provider (sets up _transforms)
|
|
260
|
+
super().__init__()
|
|
261
|
+
|
|
262
|
+
# Resolve on_duplicate from deprecated params (delete when removing deprecation)
|
|
263
|
+
self._on_duplicate: DuplicateBehaviorSetting = _resolve_on_duplicate(
|
|
264
|
+
on_duplicate,
|
|
265
|
+
on_duplicate_tools,
|
|
266
|
+
on_duplicate_resources,
|
|
267
|
+
on_duplicate_prompts,
|
|
268
|
+
)
|
|
269
|
+
|
|
196
270
|
# Resolve server default for background task support
|
|
197
271
|
self._support_tasks_by_default: bool = tasks if tasks is not None else False
|
|
198
272
|
|
|
@@ -201,26 +275,53 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
201
275
|
self._worker = None
|
|
202
276
|
|
|
203
277
|
self._additional_http_routes: list[BaseRoute] = []
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
transformations=tool_transformations,
|
|
278
|
+
|
|
279
|
+
# Session-scoped state store (shared across all requests)
|
|
280
|
+
self._state_storage: AsyncKeyValue = session_state_store or MemoryStore()
|
|
281
|
+
self._state_store: PydanticAdapter[StateValue] = PydanticAdapter[StateValue](
|
|
282
|
+
key_value=self._state_storage,
|
|
283
|
+
pydantic_model=StateValue,
|
|
284
|
+
default_collection="fastmcp_state",
|
|
212
285
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
286
|
+
|
|
287
|
+
# Create LocalProvider for local components
|
|
288
|
+
self._local_provider: LocalProvider = LocalProvider(
|
|
289
|
+
on_duplicate=self._on_duplicate
|
|
216
290
|
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
291
|
+
|
|
292
|
+
# Add providers using AggregateProvider's add_provider
|
|
293
|
+
# LocalProvider is always first (no namespace)
|
|
294
|
+
self.add_provider(self._local_provider)
|
|
295
|
+
for p in providers or []:
|
|
296
|
+
self.add_provider(p)
|
|
297
|
+
|
|
298
|
+
# Store mask_error_details for execution error handling
|
|
299
|
+
self._mask_error_details: bool = (
|
|
300
|
+
mask_error_details
|
|
301
|
+
if mask_error_details is not None
|
|
302
|
+
else fastmcp.settings.mask_error_details
|
|
220
303
|
)
|
|
304
|
+
|
|
305
|
+
# Store list_page_size for pagination of list operations
|
|
306
|
+
if list_page_size is not None and list_page_size <= 0:
|
|
307
|
+
raise ValueError("list_page_size must be a positive integer")
|
|
308
|
+
self._list_page_size: int | None = list_page_size
|
|
309
|
+
|
|
310
|
+
if tool_serializer is not None and fastmcp.settings.deprecation_warnings:
|
|
311
|
+
warnings.warn(
|
|
312
|
+
"The `tool_serializer` parameter is deprecated. "
|
|
313
|
+
"Return ToolResult from your tools for full control over serialization. "
|
|
314
|
+
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
|
|
315
|
+
DeprecationWarning,
|
|
316
|
+
stacklevel=2,
|
|
317
|
+
)
|
|
221
318
|
self._tool_serializer: Callable[[Any], str] | None = tool_serializer
|
|
222
319
|
|
|
223
|
-
|
|
320
|
+
# Handle Lifespan instances (they're callable) or regular lifespan functions
|
|
321
|
+
if lifespan is not None:
|
|
322
|
+
self._lifespan: LifespanCallable[LifespanResultT] = lifespan
|
|
323
|
+
else:
|
|
324
|
+
self._lifespan = cast(LifespanCallable[LifespanResultT], default_lifespan)
|
|
224
325
|
self._lifespan_result: LifespanResultT | None = None
|
|
225
326
|
self._lifespan_result_set: bool = False
|
|
226
327
|
self._started: asyncio.Event = asyncio.Event()
|
|
@@ -238,14 +339,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
238
339
|
lifespan=_lifespan_proxy(fastmcp_server=self),
|
|
239
340
|
)
|
|
240
341
|
|
|
241
|
-
|
|
242
|
-
if auth is NotSet:
|
|
243
|
-
if fastmcp.settings.server_auth is not None:
|
|
244
|
-
# server_auth_class returns the class itself
|
|
245
|
-
auth = fastmcp.settings.server_auth_class()
|
|
246
|
-
else:
|
|
247
|
-
auth = None
|
|
248
|
-
self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
|
|
342
|
+
self.auth: AuthProvider | None = auth
|
|
249
343
|
|
|
250
344
|
if tools:
|
|
251
345
|
for tool in tools:
|
|
@@ -253,12 +347,34 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
253
347
|
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
|
254
348
|
self.add_tool(tool)
|
|
255
349
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
350
|
+
# Handle deprecated include_tags and exclude_tags parameters
|
|
351
|
+
if include_tags is not None:
|
|
352
|
+
warnings.warn(
|
|
353
|
+
"include_tags is deprecated. Use server.enable(tags=..., only=True) instead.",
|
|
354
|
+
DeprecationWarning,
|
|
355
|
+
stacklevel=2,
|
|
356
|
+
)
|
|
357
|
+
# For backwards compatibility, initialize allowlist from include_tags
|
|
358
|
+
self.enable(tags=set(include_tags), only=True)
|
|
359
|
+
if exclude_tags is not None:
|
|
360
|
+
warnings.warn(
|
|
361
|
+
"exclude_tags is deprecated. Use server.disable(tags=...) instead.",
|
|
362
|
+
DeprecationWarning,
|
|
363
|
+
stacklevel=2,
|
|
364
|
+
)
|
|
365
|
+
# For backwards compatibility, initialize blocklist from exclude_tags
|
|
366
|
+
self.disable(tags=set(exclude_tags))
|
|
367
|
+
|
|
368
|
+
# Handle deprecated tool_transformations parameter
|
|
369
|
+
if tool_transformations:
|
|
370
|
+
if fastmcp.settings.deprecation_warnings:
|
|
371
|
+
warnings.warn(
|
|
372
|
+
"The tool_transformations parameter is deprecated. Use "
|
|
373
|
+
"server.add_transform(ToolTransform({...})) instead.",
|
|
374
|
+
DeprecationWarning,
|
|
375
|
+
stacklevel=2,
|
|
376
|
+
)
|
|
377
|
+
self._transforms.append(ToolTransform(dict(tool_transformations)))
|
|
262
378
|
|
|
263
379
|
self.strict_input_validation: bool = (
|
|
264
380
|
strict_input_validation
|
|
@@ -276,12 +392,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
276
392
|
sampling_handler_behavior or "fallback"
|
|
277
393
|
)
|
|
278
394
|
|
|
279
|
-
self.include_fastmcp_meta: bool = (
|
|
280
|
-
include_fastmcp_meta
|
|
281
|
-
if include_fastmcp_meta is not None
|
|
282
|
-
else fastmcp.settings.include_fastmcp_meta
|
|
283
|
-
)
|
|
284
|
-
|
|
285
395
|
self._handle_deprecated_settings(
|
|
286
396
|
log_level=log_level,
|
|
287
397
|
debug=debug,
|
|
@@ -374,539 +484,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
374
484
|
else:
|
|
375
485
|
return list(self._mcp_server.icons)
|
|
376
486
|
|
|
377
|
-
|
|
378
|
-
def docket(self) -> Docket | None:
|
|
379
|
-
"""Get the Docket instance if Docket support is enabled.
|
|
380
|
-
|
|
381
|
-
Returns None if Docket is not enabled or server hasn't been started yet.
|
|
382
|
-
"""
|
|
383
|
-
return self._docket
|
|
384
|
-
|
|
385
|
-
@asynccontextmanager
|
|
386
|
-
async def _docket_lifespan(self) -> AsyncIterator[None]:
|
|
387
|
-
"""Manage Docket instance and Worker for background task execution."""
|
|
388
|
-
from fastmcp import settings
|
|
389
|
-
|
|
390
|
-
# Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
|
|
391
|
-
from fastmcp.server.dependencies import (
|
|
392
|
-
_current_docket,
|
|
393
|
-
_current_server,
|
|
394
|
-
_current_worker,
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
server_token = _current_server.set(weakref.ref(self))
|
|
398
|
-
|
|
399
|
-
try:
|
|
400
|
-
# For directly mounted servers, the parent's Docket/Worker handles all
|
|
401
|
-
# task execution. Skip creating our own to avoid race conditions with
|
|
402
|
-
# multiple workers competing for tasks from the same queue.
|
|
403
|
-
if self._is_mounted:
|
|
404
|
-
yield
|
|
405
|
-
return
|
|
406
|
-
|
|
407
|
-
# Create Docket instance using configured name and URL
|
|
408
|
-
async with Docket(
|
|
409
|
-
name=settings.docket.name,
|
|
410
|
-
url=settings.docket.url,
|
|
411
|
-
) as docket:
|
|
412
|
-
# Store on server instance for cross-task access (FastMCPTransport)
|
|
413
|
-
self._docket = docket
|
|
414
|
-
|
|
415
|
-
# Register local task-enabled tools/prompts/resources with Docket
|
|
416
|
-
# Only function-based variants support background tasks
|
|
417
|
-
# Register components where task execution is not "forbidden"
|
|
418
|
-
for tool in self._tool_manager._tools.values():
|
|
419
|
-
if (
|
|
420
|
-
isinstance(tool, FunctionTool)
|
|
421
|
-
and tool.task_config.mode != "forbidden"
|
|
422
|
-
):
|
|
423
|
-
docket.register(tool.fn, names=[tool.key])
|
|
424
|
-
|
|
425
|
-
for prompt in self._prompt_manager._prompts.values():
|
|
426
|
-
if (
|
|
427
|
-
isinstance(prompt, FunctionPrompt)
|
|
428
|
-
and prompt.task_config.mode != "forbidden"
|
|
429
|
-
):
|
|
430
|
-
# task execution requires async fn (validated at creation time)
|
|
431
|
-
docket.register(
|
|
432
|
-
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
433
|
-
names=[prompt.key],
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
for resource in self._resource_manager._resources.values():
|
|
437
|
-
if (
|
|
438
|
-
isinstance(resource, FunctionResource)
|
|
439
|
-
and resource.task_config.mode != "forbidden"
|
|
440
|
-
):
|
|
441
|
-
docket.register(resource.fn, names=[resource.name])
|
|
442
|
-
|
|
443
|
-
for template in self._resource_manager._templates.values():
|
|
444
|
-
if (
|
|
445
|
-
isinstance(template, FunctionResourceTemplate)
|
|
446
|
-
and template.task_config.mode != "forbidden"
|
|
447
|
-
):
|
|
448
|
-
docket.register(template.fn, names=[template.name])
|
|
449
|
-
|
|
450
|
-
# Also register functions from mounted servers so tasks can
|
|
451
|
-
# execute in the parent's Docket context
|
|
452
|
-
for mounted in self._mounted_servers:
|
|
453
|
-
await self._register_mounted_server_functions(
|
|
454
|
-
mounted.server, docket, mounted.prefix, mounted.tool_names
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
# Set Docket in ContextVar so CurrentDocket can access it
|
|
458
|
-
docket_token = _current_docket.set(docket)
|
|
459
|
-
try:
|
|
460
|
-
# Build worker kwargs from settings
|
|
461
|
-
worker_kwargs: dict[str, Any] = {
|
|
462
|
-
"concurrency": settings.docket.concurrency,
|
|
463
|
-
"redelivery_timeout": settings.docket.redelivery_timeout,
|
|
464
|
-
"reconnection_delay": settings.docket.reconnection_delay,
|
|
465
|
-
}
|
|
466
|
-
if settings.docket.worker_name:
|
|
467
|
-
worker_kwargs["name"] = settings.docket.worker_name
|
|
468
|
-
|
|
469
|
-
# Create and start Worker
|
|
470
|
-
async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
|
|
471
|
-
# Store on server instance for cross-context access
|
|
472
|
-
self._worker = worker
|
|
473
|
-
# Set Worker in ContextVar so CurrentWorker can access it
|
|
474
|
-
worker_token = _current_worker.set(worker)
|
|
475
|
-
try:
|
|
476
|
-
worker_task = asyncio.create_task(worker.run_forever())
|
|
477
|
-
try:
|
|
478
|
-
yield
|
|
479
|
-
finally:
|
|
480
|
-
worker_task.cancel()
|
|
481
|
-
with suppress(asyncio.CancelledError):
|
|
482
|
-
await worker_task
|
|
483
|
-
finally:
|
|
484
|
-
_current_worker.reset(worker_token)
|
|
485
|
-
self._worker = None
|
|
486
|
-
finally:
|
|
487
|
-
_current_docket.reset(docket_token)
|
|
488
|
-
self._docket = None
|
|
489
|
-
finally:
|
|
490
|
-
_current_server.reset(server_token)
|
|
491
|
-
|
|
492
|
-
async def _register_mounted_server_functions(
|
|
493
|
-
self,
|
|
494
|
-
server: FastMCP,
|
|
495
|
-
docket: Docket,
|
|
496
|
-
prefix: str | None,
|
|
497
|
-
tool_names: dict[str, str] | None = None,
|
|
498
|
-
) -> None:
|
|
499
|
-
"""Register task-enabled functions from a mounted server with Docket.
|
|
500
|
-
|
|
501
|
-
This enables background task execution for mounted server components
|
|
502
|
-
through the parent server's Docket context.
|
|
503
|
-
|
|
504
|
-
Args:
|
|
505
|
-
server: The mounted server whose functions to register
|
|
506
|
-
docket: The Docket instance to register with
|
|
507
|
-
prefix: The mount prefix to prepend to function names (matches
|
|
508
|
-
client-facing tool/prompt names)
|
|
509
|
-
tool_names: Optional mapping of original tool names to custom names
|
|
510
|
-
"""
|
|
511
|
-
# Register tools with prefixed names to avoid collisions
|
|
512
|
-
for tool in server._tool_manager._tools.values():
|
|
513
|
-
if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
|
|
514
|
-
# Apply tool_names override first, then prefix (matches get_tools logic)
|
|
515
|
-
if tool_names and tool.key in tool_names:
|
|
516
|
-
fn_name = tool_names[tool.key]
|
|
517
|
-
elif prefix:
|
|
518
|
-
fn_name = f"{prefix}_{tool.key}"
|
|
519
|
-
else:
|
|
520
|
-
fn_name = tool.key
|
|
521
|
-
docket.register(tool.fn, names=[fn_name])
|
|
522
|
-
|
|
523
|
-
# Register prompts with prefixed names
|
|
524
|
-
for prompt in server._prompt_manager._prompts.values():
|
|
525
|
-
if (
|
|
526
|
-
isinstance(prompt, FunctionPrompt)
|
|
527
|
-
and prompt.task_config.mode != "forbidden"
|
|
528
|
-
):
|
|
529
|
-
fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
|
|
530
|
-
docket.register(
|
|
531
|
-
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
532
|
-
names=[fn_name],
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
# Register resources with prefixed names (use name, not key/URI)
|
|
536
|
-
for resource in server._resource_manager._resources.values():
|
|
537
|
-
if (
|
|
538
|
-
isinstance(resource, FunctionResource)
|
|
539
|
-
and resource.task_config.mode != "forbidden"
|
|
540
|
-
):
|
|
541
|
-
fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
|
|
542
|
-
docket.register(resource.fn, names=[fn_name])
|
|
543
|
-
|
|
544
|
-
# Register resource templates with prefixed names (use name, not key/URI)
|
|
545
|
-
for template in server._resource_manager._templates.values():
|
|
546
|
-
if (
|
|
547
|
-
isinstance(template, FunctionResourceTemplate)
|
|
548
|
-
and template.task_config.mode != "forbidden"
|
|
549
|
-
):
|
|
550
|
-
fn_name = f"{prefix}_{template.name}" if prefix else template.name
|
|
551
|
-
docket.register(template.fn, names=[fn_name])
|
|
552
|
-
|
|
553
|
-
# Recursively register from nested mounted servers with accumulated prefix
|
|
554
|
-
for nested in server._mounted_servers:
|
|
555
|
-
nested_prefix = (
|
|
556
|
-
f"{prefix}_{nested.prefix}"
|
|
557
|
-
if prefix and nested.prefix
|
|
558
|
-
else (prefix or nested.prefix)
|
|
559
|
-
)
|
|
560
|
-
await self._register_mounted_server_functions(
|
|
561
|
-
nested.server, docket, nested_prefix, nested.tool_names
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
@asynccontextmanager
|
|
565
|
-
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
566
|
-
if self._lifespan_result_set:
|
|
567
|
-
# Lifespan already ran - ContextVars will be set by Context.__aenter__
|
|
568
|
-
# at request time, so we just yield here.
|
|
569
|
-
yield
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
async with (
|
|
573
|
-
self._lifespan(self) as user_lifespan_result,
|
|
574
|
-
self._docket_lifespan(),
|
|
575
|
-
):
|
|
576
|
-
self._lifespan_result = user_lifespan_result
|
|
577
|
-
self._lifespan_result_set = True
|
|
578
|
-
|
|
579
|
-
async with AsyncExitStack[bool | None]() as stack:
|
|
580
|
-
for server in self._mounted_servers:
|
|
581
|
-
await stack.enter_async_context(
|
|
582
|
-
cm=server.server._lifespan_manager()
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
self._started.set()
|
|
586
|
-
try:
|
|
587
|
-
yield
|
|
588
|
-
finally:
|
|
589
|
-
self._started.clear()
|
|
590
|
-
|
|
591
|
-
self._lifespan_result_set = False
|
|
592
|
-
self._lifespan_result = None
|
|
593
|
-
|
|
594
|
-
async def run_async(
|
|
595
|
-
self,
|
|
596
|
-
transport: Transport | None = None,
|
|
597
|
-
show_banner: bool = True,
|
|
598
|
-
**transport_kwargs: Any,
|
|
599
|
-
) -> None:
|
|
600
|
-
"""Run the FastMCP server asynchronously.
|
|
601
|
-
|
|
602
|
-
Args:
|
|
603
|
-
transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
|
|
604
|
-
"""
|
|
605
|
-
if transport is None:
|
|
606
|
-
transport = "stdio"
|
|
607
|
-
if transport not in {"stdio", "http", "sse", "streamable-http"}:
|
|
608
|
-
raise ValueError(f"Unknown transport: {transport}")
|
|
609
|
-
|
|
610
|
-
if transport == "stdio":
|
|
611
|
-
await self.run_stdio_async(
|
|
612
|
-
show_banner=show_banner,
|
|
613
|
-
**transport_kwargs,
|
|
614
|
-
)
|
|
615
|
-
elif transport in {"http", "sse", "streamable-http"}:
|
|
616
|
-
await self.run_http_async(
|
|
617
|
-
transport=transport,
|
|
618
|
-
show_banner=show_banner,
|
|
619
|
-
**transport_kwargs,
|
|
620
|
-
)
|
|
621
|
-
else:
|
|
622
|
-
raise ValueError(f"Unknown transport: {transport}")
|
|
623
|
-
|
|
624
|
-
def run(
|
|
625
|
-
self,
|
|
626
|
-
transport: Transport | None = None,
|
|
627
|
-
show_banner: bool = True,
|
|
628
|
-
**transport_kwargs: Any,
|
|
629
|
-
) -> None:
|
|
630
|
-
"""Run the FastMCP server. Note this is a synchronous function.
|
|
631
|
-
|
|
632
|
-
Args:
|
|
633
|
-
transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
|
|
634
|
-
"""
|
|
635
|
-
|
|
636
|
-
anyio.run(
|
|
637
|
-
partial(
|
|
638
|
-
self.run_async,
|
|
639
|
-
transport,
|
|
640
|
-
show_banner=show_banner,
|
|
641
|
-
**transport_kwargs,
|
|
642
|
-
)
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
def _setup_handlers(self) -> None:
|
|
646
|
-
"""Set up core MCP protocol handlers."""
|
|
647
|
-
self._mcp_server.list_tools()(self._list_tools_mcp)
|
|
648
|
-
self._mcp_server.list_resources()(self._list_resources_mcp)
|
|
649
|
-
self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
|
|
650
|
-
self._mcp_server.list_prompts()(self._list_prompts_mcp)
|
|
651
|
-
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
652
|
-
self._call_tool_mcp
|
|
653
|
-
)
|
|
654
|
-
# Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
|
|
655
|
-
self._setup_read_resource_handler()
|
|
656
|
-
# Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
|
|
657
|
-
self._setup_get_prompt_handler()
|
|
658
|
-
# Register custom SEP-1686 task protocol handlers
|
|
659
|
-
self._setup_task_protocol_handlers()
|
|
660
|
-
|
|
661
|
-
def _setup_read_resource_handler(self) -> None:
|
|
662
|
-
"""
|
|
663
|
-
Set up custom read_resource handler that supports task-augmented responses.
|
|
664
|
-
|
|
665
|
-
The SDK's read_resource decorator doesn't support CreateTaskResult returns,
|
|
666
|
-
so we register a custom handler that checks request_context.experimental.is_task.
|
|
667
|
-
"""
|
|
668
|
-
|
|
669
|
-
async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
|
|
670
|
-
uri = req.params.uri
|
|
671
|
-
|
|
672
|
-
# Check for task metadata via SDK's request context
|
|
673
|
-
task_meta = None
|
|
674
|
-
try:
|
|
675
|
-
ctx = self._mcp_server.request_context
|
|
676
|
-
if ctx.experimental.is_task:
|
|
677
|
-
task_meta = ctx.experimental.task_metadata
|
|
678
|
-
except (AttributeError, LookupError):
|
|
679
|
-
pass
|
|
680
|
-
|
|
681
|
-
# Check for task metadata and route appropriately
|
|
682
|
-
async with fastmcp.server.context.Context(fastmcp=self):
|
|
683
|
-
# Get resource including from mounted servers
|
|
684
|
-
resource = await self._get_resource_or_template_or_none(str(uri))
|
|
685
|
-
if (
|
|
686
|
-
resource
|
|
687
|
-
and self._should_enable_component(resource)
|
|
688
|
-
and hasattr(resource, "task_config")
|
|
689
|
-
):
|
|
690
|
-
task_mode = resource.task_config.mode # type: ignore[union-attr]
|
|
691
|
-
|
|
692
|
-
# Enforce mode="required" - must have task metadata
|
|
693
|
-
if task_mode == "required" and not task_meta:
|
|
694
|
-
raise McpError(
|
|
695
|
-
ErrorData(
|
|
696
|
-
code=METHOD_NOT_FOUND,
|
|
697
|
-
message=f"Resource '{uri}' requires task-augmented execution",
|
|
698
|
-
)
|
|
699
|
-
)
|
|
700
|
-
|
|
701
|
-
# Route to background if task metadata present and mode allows
|
|
702
|
-
if task_meta and task_mode != "forbidden":
|
|
703
|
-
# For FunctionResource/FunctionResourceTemplate, use Docket
|
|
704
|
-
if isinstance(
|
|
705
|
-
resource,
|
|
706
|
-
FunctionResource | FunctionResourceTemplate,
|
|
707
|
-
):
|
|
708
|
-
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
709
|
-
return await handle_resource_as_task(
|
|
710
|
-
self, str(uri), resource, task_meta_dict
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
# Forbidden mode: task requested but mode="forbidden"
|
|
714
|
-
# Raise error since resources don't have isError field
|
|
715
|
-
if task_meta and task_mode == "forbidden":
|
|
716
|
-
raise McpError(
|
|
717
|
-
ErrorData(
|
|
718
|
-
code=METHOD_NOT_FOUND,
|
|
719
|
-
message=f"Resource '{uri}' does not support task-augmented execution",
|
|
720
|
-
)
|
|
721
|
-
)
|
|
722
|
-
|
|
723
|
-
# Synchronous execution
|
|
724
|
-
result = await self._read_resource_mcp(uri)
|
|
725
|
-
|
|
726
|
-
# Graceful degradation: if we got here with task_meta, something went wrong
|
|
727
|
-
# (This should be unreachable now that forbidden raises)
|
|
728
|
-
if task_meta:
|
|
729
|
-
mcp_contents = []
|
|
730
|
-
for item in result:
|
|
731
|
-
if isinstance(item.content, str):
|
|
732
|
-
mcp_contents.append(
|
|
733
|
-
mcp.types.TextResourceContents(
|
|
734
|
-
uri=uri,
|
|
735
|
-
text=item.content,
|
|
736
|
-
mimeType=item.mime_type or "text/plain",
|
|
737
|
-
)
|
|
738
|
-
)
|
|
739
|
-
elif isinstance(item.content, bytes):
|
|
740
|
-
import base64
|
|
741
|
-
|
|
742
|
-
mcp_contents.append(
|
|
743
|
-
mcp.types.BlobResourceContents(
|
|
744
|
-
uri=uri,
|
|
745
|
-
blob=base64.b64encode(item.content).decode(),
|
|
746
|
-
mimeType=item.mime_type or "application/octet-stream",
|
|
747
|
-
)
|
|
748
|
-
)
|
|
749
|
-
return mcp.types.ServerResult(
|
|
750
|
-
mcp.types.ReadResourceResult(
|
|
751
|
-
contents=mcp_contents,
|
|
752
|
-
_meta={
|
|
753
|
-
"modelcontextprotocol.io/task": {
|
|
754
|
-
"returned_immediately": True
|
|
755
|
-
}
|
|
756
|
-
},
|
|
757
|
-
)
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
# Convert to proper ServerResult
|
|
761
|
-
if isinstance(result, mcp.types.ServerResult):
|
|
762
|
-
return result
|
|
763
|
-
|
|
764
|
-
mcp_contents = []
|
|
765
|
-
for item in result:
|
|
766
|
-
if isinstance(item.content, str):
|
|
767
|
-
mcp_contents.append(
|
|
768
|
-
mcp.types.TextResourceContents(
|
|
769
|
-
uri=uri,
|
|
770
|
-
text=item.content,
|
|
771
|
-
mimeType=item.mime_type or "text/plain",
|
|
772
|
-
)
|
|
773
|
-
)
|
|
774
|
-
elif isinstance(item.content, bytes):
|
|
775
|
-
import base64
|
|
776
|
-
|
|
777
|
-
mcp_contents.append(
|
|
778
|
-
mcp.types.BlobResourceContents(
|
|
779
|
-
uri=uri,
|
|
780
|
-
blob=base64.b64encode(item.content).decode(),
|
|
781
|
-
mimeType=item.mime_type or "application/octet-stream",
|
|
782
|
-
)
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
return mcp.types.ServerResult(
|
|
786
|
-
mcp.types.ReadResourceResult(contents=mcp_contents)
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
|
|
790
|
-
|
|
791
|
-
def _setup_get_prompt_handler(self) -> None:
|
|
792
|
-
"""
|
|
793
|
-
Set up custom get_prompt handler that supports task-augmented responses.
|
|
794
|
-
|
|
795
|
-
The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
|
|
796
|
-
so we register a custom handler that checks request_context.experimental.is_task.
|
|
797
|
-
"""
|
|
798
|
-
|
|
799
|
-
async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
|
|
800
|
-
name = req.params.name
|
|
801
|
-
arguments = req.params.arguments
|
|
802
|
-
|
|
803
|
-
# Check for task metadata via SDK's request context
|
|
804
|
-
task_meta = None
|
|
805
|
-
try:
|
|
806
|
-
ctx = self._mcp_server.request_context
|
|
807
|
-
if ctx.experimental.is_task:
|
|
808
|
-
task_meta = ctx.experimental.task_metadata
|
|
809
|
-
except (AttributeError, LookupError):
|
|
810
|
-
pass
|
|
811
|
-
|
|
812
|
-
# Check for task metadata and route appropriately
|
|
813
|
-
async with fastmcp.server.context.Context(fastmcp=self):
|
|
814
|
-
prompts = await self.get_prompts()
|
|
815
|
-
prompt = prompts.get(name)
|
|
816
|
-
if (
|
|
817
|
-
prompt
|
|
818
|
-
and self._should_enable_component(prompt)
|
|
819
|
-
and hasattr(prompt, "task_config")
|
|
820
|
-
and prompt.task_config
|
|
821
|
-
):
|
|
822
|
-
task_mode = prompt.task_config.mode # type: ignore[union-attr]
|
|
823
|
-
|
|
824
|
-
# Enforce mode="required" - must have task metadata
|
|
825
|
-
if task_mode == "required" and not task_meta:
|
|
826
|
-
raise McpError(
|
|
827
|
-
ErrorData(
|
|
828
|
-
code=METHOD_NOT_FOUND,
|
|
829
|
-
message=f"Prompt '{name}' requires task-augmented execution",
|
|
830
|
-
)
|
|
831
|
-
)
|
|
832
|
-
|
|
833
|
-
# Route to background if task metadata present and mode allows
|
|
834
|
-
if task_meta and task_mode != "forbidden":
|
|
835
|
-
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
836
|
-
result = await handle_prompt_as_task(
|
|
837
|
-
self, name, arguments, task_meta_dict
|
|
838
|
-
)
|
|
839
|
-
return mcp.types.ServerResult(result)
|
|
840
|
-
|
|
841
|
-
# Forbidden mode: task requested but mode="forbidden"
|
|
842
|
-
# Raise error since prompts don't have isError field
|
|
843
|
-
if task_meta and task_mode == "forbidden":
|
|
844
|
-
raise McpError(
|
|
845
|
-
ErrorData(
|
|
846
|
-
code=METHOD_NOT_FOUND,
|
|
847
|
-
message=f"Prompt '{name}' does not support task-augmented execution",
|
|
848
|
-
)
|
|
849
|
-
)
|
|
850
|
-
|
|
851
|
-
# Synchronous execution
|
|
852
|
-
result = await self._get_prompt_mcp(name, arguments)
|
|
853
|
-
return mcp.types.ServerResult(result)
|
|
854
|
-
|
|
855
|
-
self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
|
|
856
|
-
|
|
857
|
-
def _setup_task_protocol_handlers(self) -> None:
|
|
858
|
-
"""Register SEP-1686 task protocol handlers with SDK."""
|
|
859
|
-
from mcp.types import (
|
|
860
|
-
CancelTaskRequest,
|
|
861
|
-
GetTaskPayloadRequest,
|
|
862
|
-
GetTaskRequest,
|
|
863
|
-
ListTasksRequest,
|
|
864
|
-
ServerResult,
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
from fastmcp.server.tasks.protocol import (
|
|
868
|
-
tasks_cancel_handler,
|
|
869
|
-
tasks_get_handler,
|
|
870
|
-
tasks_list_handler,
|
|
871
|
-
tasks_result_handler,
|
|
872
|
-
)
|
|
873
|
-
|
|
874
|
-
# Manually register handlers (SDK decorators fail with locally-defined functions)
|
|
875
|
-
# SDK expects handlers that receive Request objects and return ServerResult
|
|
876
|
-
|
|
877
|
-
async def handle_get_task(req: GetTaskRequest) -> ServerResult:
|
|
878
|
-
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
879
|
-
result = await tasks_get_handler(self, params)
|
|
880
|
-
return ServerResult(result)
|
|
881
|
-
|
|
882
|
-
async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
|
|
883
|
-
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
884
|
-
result = await tasks_result_handler(self, params)
|
|
885
|
-
return ServerResult(result)
|
|
886
|
-
|
|
887
|
-
async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
|
|
888
|
-
params = (
|
|
889
|
-
req.params.model_dump(by_alias=True, exclude_none=True)
|
|
890
|
-
if req.params
|
|
891
|
-
else {}
|
|
892
|
-
)
|
|
893
|
-
result = await tasks_list_handler(self, params)
|
|
894
|
-
return ServerResult(result)
|
|
895
|
-
|
|
896
|
-
async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
|
|
897
|
-
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
898
|
-
result = await tasks_cancel_handler(self, params)
|
|
899
|
-
return ServerResult(result)
|
|
900
|
-
|
|
901
|
-
# Register directly with SDK (same as what decorators do internally)
|
|
902
|
-
self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
|
|
903
|
-
self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
|
|
904
|
-
handle_get_task_result
|
|
905
|
-
)
|
|
906
|
-
self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
|
|
907
|
-
self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
|
|
908
|
-
|
|
909
|
-
async def _apply_middleware(
|
|
487
|
+
async def _run_middleware(
|
|
910
488
|
self,
|
|
911
489
|
context: MiddlewareContext[Any],
|
|
912
490
|
call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
|
|
@@ -920,999 +498,862 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
920
498
|
def add_middleware(self, middleware: Middleware) -> None:
|
|
921
499
|
self.middleware.append(middleware)
|
|
922
500
|
|
|
923
|
-
|
|
924
|
-
"""
|
|
925
|
-
all_tools = dict(await self._tool_manager.get_tools())
|
|
501
|
+
def add_provider(self, provider: Provider, *, namespace: str = "") -> None:
|
|
502
|
+
"""Add a provider for dynamic tools, resources, and prompts.
|
|
926
503
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
for key, tool in child_tools.items():
|
|
931
|
-
# Check for manual override first, then apply prefix
|
|
932
|
-
if mounted.tool_names and key in mounted.tool_names:
|
|
933
|
-
new_key = mounted.tool_names[key]
|
|
934
|
-
elif mounted.prefix:
|
|
935
|
-
new_key = f"{mounted.prefix}_{key}"
|
|
936
|
-
else:
|
|
937
|
-
new_key = key
|
|
938
|
-
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
939
|
-
except Exception as e:
|
|
940
|
-
logger.warning(
|
|
941
|
-
f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
|
|
942
|
-
)
|
|
943
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
944
|
-
raise
|
|
945
|
-
continue
|
|
504
|
+
Providers are queried in registration order. The first provider to return
|
|
505
|
+
a non-None result wins. Static components (registered via decorators)
|
|
506
|
+
always take precedence over providers.
|
|
946
507
|
|
|
947
|
-
|
|
508
|
+
Args:
|
|
509
|
+
provider: A Provider instance that will provide components dynamically.
|
|
510
|
+
namespace: Optional namespace prefix. When set:
|
|
511
|
+
- Tools become "namespace_toolname"
|
|
512
|
+
- Resources become "protocol://namespace/path"
|
|
513
|
+
- Prompts become "namespace_promptname"
|
|
514
|
+
"""
|
|
515
|
+
super().add_provider(provider, namespace=namespace)
|
|
516
|
+
|
|
517
|
+
# -------------------------------------------------------------------------
|
|
518
|
+
# Provider interface overrides - inherited from AggregateProvider
|
|
519
|
+
# -------------------------------------------------------------------------
|
|
520
|
+
# _list_tools, _list_resources, _list_resource_templates, _list_prompts
|
|
521
|
+
# are inherited from AggregateProvider which handles aggregation and namespacing
|
|
522
|
+
|
|
523
|
+
async def get_tasks(self) -> Sequence[FastMCPComponent]:
|
|
524
|
+
"""Get task-eligible components with all transforms applied.
|
|
525
|
+
|
|
526
|
+
Overrides AggregateProvider.get_tasks() to apply server-level transforms
|
|
527
|
+
after aggregation. AggregateProvider handles provider-level namespacing.
|
|
528
|
+
"""
|
|
529
|
+
# Get tasks from AggregateProvider (handles aggregation and namespacing)
|
|
530
|
+
components = list(await super().get_tasks())
|
|
531
|
+
|
|
532
|
+
# Separate by component type for server-level transform application
|
|
533
|
+
tools = [c for c in components if isinstance(c, Tool)]
|
|
534
|
+
resources = [c for c in components if isinstance(c, Resource)]
|
|
535
|
+
templates = [c for c in components if isinstance(c, ResourceTemplate)]
|
|
536
|
+
prompts = [c for c in components if isinstance(c, Prompt)]
|
|
537
|
+
|
|
538
|
+
# Apply server-level transforms sequentially
|
|
539
|
+
for transform in self.transforms:
|
|
540
|
+
tools = await transform.list_tools(tools)
|
|
541
|
+
resources = await transform.list_resources(resources)
|
|
542
|
+
templates = await transform.list_resource_templates(templates)
|
|
543
|
+
prompts = await transform.list_prompts(prompts)
|
|
544
|
+
|
|
545
|
+
return [
|
|
546
|
+
*tools,
|
|
547
|
+
*resources,
|
|
548
|
+
*templates,
|
|
549
|
+
*prompts,
|
|
550
|
+
]
|
|
551
|
+
|
|
552
|
+
def add_transform(self, transform: Transform) -> None:
|
|
553
|
+
"""Add a server-level transform.
|
|
948
554
|
|
|
949
|
-
|
|
950
|
-
tools
|
|
951
|
-
if key not in tools:
|
|
952
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
953
|
-
return tools[key]
|
|
555
|
+
Server-level transforms are applied after all providers are aggregated.
|
|
556
|
+
They transform tools, resources, and prompts from ALL providers.
|
|
954
557
|
|
|
955
|
-
|
|
956
|
-
|
|
558
|
+
Args:
|
|
559
|
+
transform: The transform to add.
|
|
957
560
|
|
|
958
|
-
|
|
959
|
-
|
|
561
|
+
Example:
|
|
562
|
+
```python
|
|
563
|
+
from fastmcp.server.transforms import Namespace
|
|
564
|
+
|
|
565
|
+
server = FastMCP("Server")
|
|
566
|
+
server.add_transform(Namespace("api"))
|
|
567
|
+
# All tools from all providers become "api_toolname"
|
|
568
|
+
```
|
|
960
569
|
"""
|
|
961
|
-
|
|
962
|
-
return await self.get_tool(key)
|
|
963
|
-
except NotFoundError:
|
|
964
|
-
return None
|
|
570
|
+
self._transforms.append(transform)
|
|
965
571
|
|
|
966
|
-
|
|
967
|
-
self,
|
|
968
|
-
) ->
|
|
969
|
-
"""
|
|
970
|
-
try:
|
|
971
|
-
return await self.get_resource(uri)
|
|
972
|
-
except NotFoundError:
|
|
973
|
-
pass
|
|
572
|
+
def add_tool_transformation(
|
|
573
|
+
self, tool_name: str, transformation: ToolTransformConfig
|
|
574
|
+
) -> None:
|
|
575
|
+
"""Add a tool transformation.
|
|
974
576
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
577
|
+
.. deprecated::
|
|
578
|
+
Use ``add_transform(ToolTransform({...}))`` instead.
|
|
579
|
+
"""
|
|
580
|
+
if fastmcp.settings.deprecation_warnings:
|
|
581
|
+
warnings.warn(
|
|
582
|
+
"add_tool_transformation is deprecated. Use "
|
|
583
|
+
"server.add_transform(ToolTransform({tool_name: config})) instead.",
|
|
584
|
+
DeprecationWarning,
|
|
585
|
+
stacklevel=2,
|
|
586
|
+
)
|
|
587
|
+
self.add_transform(ToolTransform({tool_name: transformation}))
|
|
979
588
|
|
|
980
|
-
|
|
589
|
+
def remove_tool_transformation(self, _tool_name: str) -> None:
|
|
590
|
+
"""Remove a tool transformation.
|
|
981
591
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
592
|
+
.. deprecated::
|
|
593
|
+
Tool transformations are now immutable. Use enable/disable controls instead.
|
|
594
|
+
"""
|
|
595
|
+
if fastmcp.settings.deprecation_warnings:
|
|
596
|
+
warnings.warn(
|
|
597
|
+
"remove_tool_transformation is deprecated and has no effect. "
|
|
598
|
+
"Transforms are immutable once added. Use server.disable(keys=[...]) "
|
|
599
|
+
"to hide tools instead.",
|
|
600
|
+
DeprecationWarning,
|
|
601
|
+
stacklevel=2,
|
|
602
|
+
)
|
|
985
603
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
|
|
604
|
+
async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]:
|
|
605
|
+
"""List all enabled tools from providers.
|
|
606
|
+
|
|
607
|
+
Overrides Provider.list_tools() to add visibility filtering, auth filtering,
|
|
608
|
+
and middleware execution. Returns all versions (no deduplication).
|
|
609
|
+
Protocol handlers deduplicate for MCP wire format.
|
|
610
|
+
"""
|
|
611
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
612
|
+
if run_middleware:
|
|
613
|
+
mw_context = MiddlewareContext(
|
|
614
|
+
message=mcp.types.ListToolsRequest(method="tools/list"),
|
|
615
|
+
source="client",
|
|
616
|
+
type="request",
|
|
617
|
+
method="tools/list",
|
|
618
|
+
fastmcp_context=ctx,
|
|
619
|
+
)
|
|
620
|
+
return await self._run_middleware(
|
|
621
|
+
context=mw_context,
|
|
622
|
+
call_next=lambda context: self.list_tools(run_middleware=False),
|
|
1006
623
|
)
|
|
1007
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1008
|
-
raise
|
|
1009
|
-
continue
|
|
1010
|
-
|
|
1011
|
-
return all_resources
|
|
1012
624
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
return resources[key]
|
|
625
|
+
# Get all tools, apply session transforms, then filter enabled
|
|
626
|
+
tools = list(await super().list_tools())
|
|
627
|
+
tools = await apply_session_transforms(tools)
|
|
628
|
+
tools = [t for t in tools if is_enabled(t)]
|
|
1018
629
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
630
|
+
skip_auth, token = _get_auth_context()
|
|
631
|
+
authorized: list[Tool] = []
|
|
632
|
+
for tool in tools:
|
|
633
|
+
if not skip_auth and tool.auth is not None:
|
|
634
|
+
ctx = AuthContext(token=token, component=tool)
|
|
635
|
+
try:
|
|
636
|
+
if not run_auth_checks(tool.auth, ctx):
|
|
637
|
+
continue
|
|
638
|
+
except AuthorizationError:
|
|
639
|
+
continue
|
|
640
|
+
authorized.append(tool)
|
|
641
|
+
return authorized
|
|
1022
642
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
new_key = (
|
|
1028
|
-
add_resource_prefix(key, mounted.prefix)
|
|
1029
|
-
if mounted.prefix
|
|
1030
|
-
else key
|
|
1031
|
-
)
|
|
1032
|
-
update: dict[str, Any] = {}
|
|
1033
|
-
if mounted.prefix:
|
|
1034
|
-
if template.name:
|
|
1035
|
-
update["name"] = f"{mounted.prefix}_{template.name}"
|
|
1036
|
-
# Update uri_template so matches() works with prefixed URIs
|
|
1037
|
-
update["uri_template"] = new_key
|
|
1038
|
-
all_templates[new_key] = template.model_copy(
|
|
1039
|
-
key=new_key, update=update
|
|
1040
|
-
)
|
|
1041
|
-
except Exception as e:
|
|
1042
|
-
logger.warning(
|
|
1043
|
-
f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
|
|
1044
|
-
)
|
|
1045
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1046
|
-
raise
|
|
1047
|
-
continue
|
|
643
|
+
async def _get_tool(
|
|
644
|
+
self, name: str, version: VersionSpec | None = None
|
|
645
|
+
) -> Tool | None:
|
|
646
|
+
"""Get a tool by name via aggregation from providers.
|
|
1048
647
|
|
|
1049
|
-
|
|
648
|
+
Extends AggregateProvider._get_tool() with component-level auth checks.
|
|
1050
649
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
if key not in templates:
|
|
1055
|
-
raise NotFoundError(f"Unknown resource template: {key}")
|
|
1056
|
-
return templates[key]
|
|
650
|
+
Args:
|
|
651
|
+
name: The tool name.
|
|
652
|
+
version: Version filter (None returns highest version).
|
|
1057
653
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
654
|
+
Returns:
|
|
655
|
+
The tool if found and authorized, None if not found or unauthorized.
|
|
656
|
+
"""
|
|
657
|
+
# Get tool from AggregateProvider (handles aggregation and namespacing)
|
|
658
|
+
tool = await super()._get_tool(name, version)
|
|
659
|
+
if tool is None:
|
|
660
|
+
return None
|
|
1061
661
|
|
|
1062
|
-
|
|
662
|
+
# Component auth - return None if unauthorized (consistent with list filtering)
|
|
663
|
+
skip_auth, token = _get_auth_context()
|
|
664
|
+
if not skip_auth and tool.auth is not None:
|
|
665
|
+
ctx = AuthContext(token=token, component=tool)
|
|
1063
666
|
try:
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
except Exception as e:
|
|
1069
|
-
logger.warning(
|
|
1070
|
-
f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
|
|
1071
|
-
)
|
|
1072
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1073
|
-
raise
|
|
1074
|
-
continue
|
|
667
|
+
if not run_auth_checks(tool.auth, ctx):
|
|
668
|
+
return None
|
|
669
|
+
except AuthorizationError:
|
|
670
|
+
return None
|
|
1075
671
|
|
|
1076
|
-
return
|
|
672
|
+
return tool
|
|
1077
673
|
|
|
1078
|
-
async def
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
return prompts[key]
|
|
674
|
+
async def get_tool(
|
|
675
|
+
self, name: str, version: VersionSpec | None = None
|
|
676
|
+
) -> Tool | None:
|
|
677
|
+
"""Get a tool by name, filtering disabled tools.
|
|
1083
678
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
methods: list[str],
|
|
1088
|
-
name: str | None = None,
|
|
1089
|
-
include_in_schema: bool = True,
|
|
1090
|
-
) -> Callable[
|
|
1091
|
-
[Callable[[Request], Awaitable[Response]]],
|
|
1092
|
-
Callable[[Request], Awaitable[Response]],
|
|
1093
|
-
]:
|
|
1094
|
-
"""
|
|
1095
|
-
Decorator to register a custom HTTP route on the FastMCP server.
|
|
1096
|
-
|
|
1097
|
-
Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,
|
|
1098
|
-
which can be useful for OAuth callbacks, health checks, or admin APIs.
|
|
1099
|
-
The handler function must be an async function that accepts a Starlette
|
|
1100
|
-
Request and returns a Response.
|
|
679
|
+
Overrides Provider.get_tool() to add visibility filtering after all
|
|
680
|
+
transforms (including session-level) have been applied. This ensures
|
|
681
|
+
session transforms can override provider-level disables.
|
|
1101
682
|
|
|
1102
683
|
Args:
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
name: Optional name for the route (to reference this route with
|
|
1106
|
-
Starlette's reverse URL lookup feature)
|
|
1107
|
-
include_in_schema: Whether to include in OpenAPI schema, defaults to True
|
|
684
|
+
name: The tool name.
|
|
685
|
+
version: Version filter (None returns highest version).
|
|
1108
686
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
```python
|
|
1112
|
-
@server.custom_route("/health", methods=["GET"])
|
|
1113
|
-
async def health_check(request: Request) -> Response:
|
|
1114
|
-
return JSONResponse({"status": "ok"})
|
|
1115
|
-
```
|
|
687
|
+
Returns:
|
|
688
|
+
The tool if found and enabled, None otherwise.
|
|
1116
689
|
"""
|
|
690
|
+
tool = await super().get_tool(name, version)
|
|
691
|
+
if tool is None:
|
|
692
|
+
return None
|
|
1117
693
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
694
|
+
# Apply session transforms to single item
|
|
695
|
+
tools = await apply_session_transforms([tool])
|
|
696
|
+
if not tools or not is_enabled(tools[0]):
|
|
697
|
+
return None
|
|
698
|
+
return tools[0]
|
|
699
|
+
|
|
700
|
+
async def list_resources(
|
|
701
|
+
self, *, run_middleware: bool = True
|
|
702
|
+
) -> Sequence[Resource]:
|
|
703
|
+
"""List all enabled resources from providers.
|
|
704
|
+
|
|
705
|
+
Overrides Provider.list_resources() to add visibility filtering, auth filtering,
|
|
706
|
+
and middleware execution. Returns all versions (no deduplication).
|
|
707
|
+
Protocol handlers deduplicate for MCP wire format.
|
|
708
|
+
"""
|
|
709
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
710
|
+
if run_middleware:
|
|
711
|
+
mw_context = MiddlewareContext(
|
|
712
|
+
message={},
|
|
713
|
+
source="client",
|
|
714
|
+
type="request",
|
|
715
|
+
method="resources/list",
|
|
716
|
+
fastmcp_context=ctx,
|
|
717
|
+
)
|
|
718
|
+
return await self._run_middleware(
|
|
719
|
+
context=mw_context,
|
|
720
|
+
call_next=lambda context: self.list_resources(run_middleware=False),
|
|
1128
721
|
)
|
|
1129
|
-
)
|
|
1130
|
-
return fn
|
|
1131
722
|
|
|
1132
|
-
|
|
723
|
+
# Get all resources, apply session transforms, then filter enabled
|
|
724
|
+
resources = list(await super().list_resources())
|
|
725
|
+
resources = await apply_session_transforms(resources)
|
|
726
|
+
resources = [r for r in resources if is_enabled(r)]
|
|
727
|
+
|
|
728
|
+
skip_auth, token = _get_auth_context()
|
|
729
|
+
authorized: list[Resource] = []
|
|
730
|
+
for resource in resources:
|
|
731
|
+
if not skip_auth and resource.auth is not None:
|
|
732
|
+
ctx = AuthContext(token=token, component=resource)
|
|
733
|
+
try:
|
|
734
|
+
if not run_auth_checks(resource.auth, ctx):
|
|
735
|
+
continue
|
|
736
|
+
except AuthorizationError:
|
|
737
|
+
continue
|
|
738
|
+
authorized.append(resource)
|
|
739
|
+
return authorized
|
|
740
|
+
|
|
741
|
+
async def _get_resource(
|
|
742
|
+
self, uri: str, version: VersionSpec | None = None
|
|
743
|
+
) -> Resource | None:
|
|
744
|
+
"""Get a resource by URI via aggregation from providers.
|
|
1133
745
|
|
|
1134
|
-
|
|
1135
|
-
"""Get all additional HTTP routes including from mounted servers.
|
|
746
|
+
Extends AggregateProvider._get_resource() with component-level auth checks.
|
|
1136
747
|
|
|
1137
|
-
|
|
1138
|
-
|
|
748
|
+
Args:
|
|
749
|
+
uri: The resource URI.
|
|
750
|
+
version: Version filter (None returns highest version).
|
|
1139
751
|
|
|
1140
752
|
Returns:
|
|
1141
|
-
|
|
753
|
+
The resource if found and authorized, None if not found or unauthorized.
|
|
1142
754
|
"""
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
mounted_routes = mounted_server.server._get_additional_http_routes()
|
|
1148
|
-
routes.extend(mounted_routes)
|
|
755
|
+
# Get resource from AggregateProvider (handles aggregation and namespacing)
|
|
756
|
+
resource = await super()._get_resource(uri, version)
|
|
757
|
+
if resource is None:
|
|
758
|
+
return None
|
|
1149
759
|
|
|
1150
|
-
return
|
|
760
|
+
# Component auth - return None if unauthorized (consistent with list filtering)
|
|
761
|
+
skip_auth, token = _get_auth_context()
|
|
762
|
+
if not skip_auth and resource.auth is not None:
|
|
763
|
+
ctx = AuthContext(token=token, component=resource)
|
|
764
|
+
try:
|
|
765
|
+
if not run_auth_checks(resource.auth, ctx):
|
|
766
|
+
return None
|
|
767
|
+
except AuthorizationError:
|
|
768
|
+
return None
|
|
1151
769
|
|
|
1152
|
-
|
|
1153
|
-
"""
|
|
1154
|
-
List all available tools, in the format expected by the low-level MCP
|
|
1155
|
-
server.
|
|
1156
|
-
"""
|
|
1157
|
-
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
1158
|
-
|
|
1159
|
-
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1160
|
-
tools = await self._list_tools_middleware()
|
|
1161
|
-
return [
|
|
1162
|
-
tool.to_mcp_tool(
|
|
1163
|
-
name=tool.key,
|
|
1164
|
-
include_fastmcp_meta=self.include_fastmcp_meta,
|
|
1165
|
-
)
|
|
1166
|
-
for tool in tools
|
|
1167
|
-
]
|
|
770
|
+
return resource
|
|
1168
771
|
|
|
1169
|
-
async def
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
"""
|
|
772
|
+
async def get_resource(
|
|
773
|
+
self, uri: str, version: VersionSpec | None = None
|
|
774
|
+
) -> Resource | None:
|
|
775
|
+
"""Get a resource by URI, filtering disabled resources.
|
|
1173
776
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
mw_context = MiddlewareContext(
|
|
1177
|
-
message=mcp.types.ListToolsRequest(method="tools/list"),
|
|
1178
|
-
source="client",
|
|
1179
|
-
type="request",
|
|
1180
|
-
method="tools/list",
|
|
1181
|
-
fastmcp_context=fastmcp_ctx,
|
|
1182
|
-
)
|
|
777
|
+
Overrides Provider.get_resource() to add visibility filtering after all
|
|
778
|
+
transforms (including session-level) have been applied.
|
|
1183
779
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
context=mw_context, call_next=self._list_tools
|
|
1188
|
-
)
|
|
1189
|
-
)
|
|
780
|
+
Args:
|
|
781
|
+
uri: The resource URI.
|
|
782
|
+
version: Version filter (None returns highest version).
|
|
1190
783
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
1194
|
-
) -> list[Tool]:
|
|
1195
|
-
"""
|
|
1196
|
-
List all available tools.
|
|
784
|
+
Returns:
|
|
785
|
+
The resource if found and enabled, None otherwise.
|
|
1197
786
|
"""
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
tool for tool in local_tools.values() if self._should_enable_component(tool)
|
|
1202
|
-
]
|
|
1203
|
-
|
|
1204
|
-
# 2. Get tools from mounted servers
|
|
1205
|
-
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1206
|
-
# Use a dict to implement "later wins" deduplication by key
|
|
1207
|
-
all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
|
|
1208
|
-
|
|
1209
|
-
for mounted in self._mounted_servers:
|
|
1210
|
-
try:
|
|
1211
|
-
child_tools = await mounted.server._list_tools_middleware()
|
|
1212
|
-
for tool in child_tools:
|
|
1213
|
-
# Apply parent server's filtering to mounted components
|
|
1214
|
-
if not self._should_enable_component(tool):
|
|
1215
|
-
continue
|
|
787
|
+
resource = await super().get_resource(uri, version)
|
|
788
|
+
if resource is None:
|
|
789
|
+
return None
|
|
1216
790
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
791
|
+
# Apply session transforms to single item
|
|
792
|
+
resources = await apply_session_transforms([resource])
|
|
793
|
+
if not resources or not is_enabled(resources[0]):
|
|
794
|
+
return None
|
|
795
|
+
return resources[0]
|
|
796
|
+
|
|
797
|
+
async def list_resource_templates(
|
|
798
|
+
self, *, run_middleware: bool = True
|
|
799
|
+
) -> Sequence[ResourceTemplate]:
|
|
800
|
+
"""List all enabled resource templates from providers.
|
|
801
|
+
|
|
802
|
+
Overrides Provider.list_resource_templates() to add visibility filtering,
|
|
803
|
+
auth filtering, and middleware execution. Returns all versions (no deduplication).
|
|
804
|
+
Protocol handlers deduplicate for MCP wire format.
|
|
805
|
+
"""
|
|
806
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
807
|
+
if run_middleware:
|
|
808
|
+
mw_context = MiddlewareContext(
|
|
809
|
+
message={},
|
|
810
|
+
source="client",
|
|
811
|
+
type="request",
|
|
812
|
+
method="resources/templates/list",
|
|
813
|
+
fastmcp_context=ctx,
|
|
1232
814
|
)
|
|
1233
|
-
|
|
1234
|
-
|
|
815
|
+
return await self._run_middleware(
|
|
816
|
+
context=mw_context,
|
|
817
|
+
call_next=lambda context: self.list_resource_templates(
|
|
818
|
+
run_middleware=False
|
|
819
|
+
),
|
|
1235
820
|
)
|
|
1236
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1237
|
-
raise
|
|
1238
|
-
continue
|
|
1239
821
|
|
|
1240
|
-
|
|
822
|
+
# Get all templates, apply session transforms, then filter enabled
|
|
823
|
+
templates = list(await super().list_resource_templates())
|
|
824
|
+
templates = await apply_session_transforms(templates)
|
|
825
|
+
templates = [t for t in templates if is_enabled(t)]
|
|
826
|
+
|
|
827
|
+
skip_auth, token = _get_auth_context()
|
|
828
|
+
authorized: list[ResourceTemplate] = []
|
|
829
|
+
for template in templates:
|
|
830
|
+
if not skip_auth and template.auth is not None:
|
|
831
|
+
ctx = AuthContext(token=token, component=template)
|
|
832
|
+
try:
|
|
833
|
+
if not run_auth_checks(template.auth, ctx):
|
|
834
|
+
continue
|
|
835
|
+
except AuthorizationError:
|
|
836
|
+
continue
|
|
837
|
+
authorized.append(template)
|
|
838
|
+
return authorized
|
|
1241
839
|
|
|
1242
|
-
async def
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
"""
|
|
1247
|
-
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
1248
|
-
|
|
1249
|
-
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1250
|
-
resources = await self._list_resources_middleware()
|
|
1251
|
-
return [
|
|
1252
|
-
resource.to_mcp_resource(
|
|
1253
|
-
uri=resource.key,
|
|
1254
|
-
include_fastmcp_meta=self.include_fastmcp_meta,
|
|
1255
|
-
)
|
|
1256
|
-
for resource in resources
|
|
1257
|
-
]
|
|
840
|
+
async def _get_resource_template(
|
|
841
|
+
self, uri: str, version: VersionSpec | None = None
|
|
842
|
+
) -> ResourceTemplate | None:
|
|
843
|
+
"""Get a resource template by URI via aggregation from providers.
|
|
1258
844
|
|
|
1259
|
-
|
|
1260
|
-
"""
|
|
1261
|
-
List all available resources, applying MCP middleware.
|
|
1262
|
-
"""
|
|
1263
|
-
|
|
1264
|
-
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
1265
|
-
# Create the middleware context.
|
|
1266
|
-
mw_context = MiddlewareContext(
|
|
1267
|
-
message={}, # List resources doesn't have parameters
|
|
1268
|
-
source="client",
|
|
1269
|
-
type="request",
|
|
1270
|
-
method="resources/list",
|
|
1271
|
-
fastmcp_context=fastmcp_ctx,
|
|
1272
|
-
)
|
|
845
|
+
Extends AggregateProvider._get_resource_template() with component-level auth checks.
|
|
1273
846
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
context=mw_context, call_next=self._list_resources
|
|
1278
|
-
)
|
|
1279
|
-
)
|
|
847
|
+
Args:
|
|
848
|
+
uri: The template URI to match.
|
|
849
|
+
version: Version filter (None returns highest version).
|
|
1280
850
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
context: MiddlewareContext[dict[str, Any]],
|
|
1284
|
-
) -> list[Resource]:
|
|
1285
|
-
"""
|
|
1286
|
-
List all available resources.
|
|
851
|
+
Returns:
|
|
852
|
+
The template if found and authorized, None if not found or unauthorized.
|
|
1287
853
|
"""
|
|
1288
|
-
#
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
for resource in local_resources.values()
|
|
1293
|
-
if self._should_enable_component(resource)
|
|
1294
|
-
]
|
|
1295
|
-
|
|
1296
|
-
# 2. Get from mounted servers with resource prefix handling
|
|
1297
|
-
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1298
|
-
# Use a dict to implement "later wins" deduplication by key
|
|
1299
|
-
all_resources: dict[str, Resource] = {
|
|
1300
|
-
resource.key: resource for resource in filtered_local
|
|
1301
|
-
}
|
|
854
|
+
# Get template from AggregateProvider (handles aggregation and namespacing)
|
|
855
|
+
template = await super()._get_resource_template(uri, version)
|
|
856
|
+
if template is None:
|
|
857
|
+
return None
|
|
1302
858
|
|
|
1303
|
-
|
|
859
|
+
# Component auth - return None if unauthorized (consistent with list filtering)
|
|
860
|
+
skip_auth, token = _get_auth_context()
|
|
861
|
+
if not skip_auth and template.auth is not None:
|
|
862
|
+
ctx = AuthContext(token=token, component=template)
|
|
1304
863
|
try:
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
continue
|
|
1310
|
-
|
|
1311
|
-
key = resource.key
|
|
1312
|
-
if mounted.prefix:
|
|
1313
|
-
key = add_resource_prefix(resource.key, mounted.prefix)
|
|
1314
|
-
resource = resource.model_copy(
|
|
1315
|
-
key=key,
|
|
1316
|
-
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
1317
|
-
)
|
|
1318
|
-
# Later mounted servers override earlier ones
|
|
1319
|
-
all_resources[key] = resource
|
|
1320
|
-
except Exception as e:
|
|
1321
|
-
server_name = getattr(
|
|
1322
|
-
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1323
|
-
)
|
|
1324
|
-
logger.warning(f"Failed to list resources from {server_name!r}: {e}")
|
|
1325
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1326
|
-
raise
|
|
1327
|
-
continue
|
|
864
|
+
if not run_auth_checks(template.auth, ctx):
|
|
865
|
+
return None
|
|
866
|
+
except AuthorizationError:
|
|
867
|
+
return None
|
|
1328
868
|
|
|
1329
|
-
return
|
|
1330
|
-
|
|
1331
|
-
async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]:
|
|
1332
|
-
"""
|
|
1333
|
-
List all available resource templates, in the format expected by the low-level MCP
|
|
1334
|
-
server.
|
|
1335
|
-
"""
|
|
1336
|
-
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
1337
|
-
|
|
1338
|
-
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1339
|
-
templates = await self._list_resource_templates_middleware()
|
|
1340
|
-
return [
|
|
1341
|
-
template.to_mcp_template(
|
|
1342
|
-
uriTemplate=template.key,
|
|
1343
|
-
include_fastmcp_meta=self.include_fastmcp_meta,
|
|
1344
|
-
)
|
|
1345
|
-
for template in templates
|
|
1346
|
-
]
|
|
1347
|
-
|
|
1348
|
-
async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
|
|
1349
|
-
"""
|
|
1350
|
-
List all available resource templates, applying MCP middleware.
|
|
869
|
+
return template
|
|
1351
870
|
|
|
1352
|
-
|
|
871
|
+
async def get_resource_template(
|
|
872
|
+
self, uri: str, version: VersionSpec | None = None
|
|
873
|
+
) -> ResourceTemplate | None:
|
|
874
|
+
"""Get a resource template by URI, filtering disabled templates.
|
|
1353
875
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
mw_context = MiddlewareContext(
|
|
1357
|
-
message={}, # List resource templates doesn't have parameters
|
|
1358
|
-
source="client",
|
|
1359
|
-
type="request",
|
|
1360
|
-
method="resources/templates/list",
|
|
1361
|
-
fastmcp_context=fastmcp_ctx,
|
|
1362
|
-
)
|
|
876
|
+
Overrides Provider.get_resource_template() to add visibility filtering after
|
|
877
|
+
all transforms (including session-level) have been applied.
|
|
1363
878
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
context=mw_context, call_next=self._list_resource_templates
|
|
1368
|
-
)
|
|
1369
|
-
)
|
|
879
|
+
Args:
|
|
880
|
+
uri: The template URI.
|
|
881
|
+
version: Version filter (None returns highest version).
|
|
1370
882
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
context: MiddlewareContext[dict[str, Any]],
|
|
1374
|
-
) -> list[ResourceTemplate]:
|
|
1375
|
-
"""
|
|
1376
|
-
List all available resource templates.
|
|
883
|
+
Returns:
|
|
884
|
+
The template if found and enabled, None otherwise.
|
|
1377
885
|
"""
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
template
|
|
1382
|
-
for template in local_templates.values()
|
|
1383
|
-
if self._should_enable_component(template)
|
|
1384
|
-
]
|
|
1385
|
-
|
|
1386
|
-
# 2. Get from mounted servers with resource prefix handling
|
|
1387
|
-
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1388
|
-
# Use a dict to implement "later wins" deduplication by key
|
|
1389
|
-
all_templates: dict[str, ResourceTemplate] = {
|
|
1390
|
-
template.key: template for template in filtered_local
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
for mounted in self._mounted_servers:
|
|
1394
|
-
try:
|
|
1395
|
-
child_templates = (
|
|
1396
|
-
await mounted.server._list_resource_templates_middleware()
|
|
1397
|
-
)
|
|
1398
|
-
for template in child_templates:
|
|
1399
|
-
# Apply parent server's filtering to mounted components
|
|
1400
|
-
if not self._should_enable_component(template):
|
|
1401
|
-
continue
|
|
886
|
+
template = await super().get_resource_template(uri, version)
|
|
887
|
+
if template is None:
|
|
888
|
+
return None
|
|
1402
889
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
890
|
+
# Apply session transforms to single item
|
|
891
|
+
templates = await apply_session_transforms([template])
|
|
892
|
+
if not templates or not is_enabled(templates[0]):
|
|
893
|
+
return None
|
|
894
|
+
return templates[0]
|
|
895
|
+
|
|
896
|
+
async def list_prompts(self, *, run_middleware: bool = True) -> Sequence[Prompt]:
|
|
897
|
+
"""List all enabled prompts from providers.
|
|
898
|
+
|
|
899
|
+
Overrides Provider.list_prompts() to add visibility filtering, auth filtering,
|
|
900
|
+
and middleware execution. Returns all versions (no deduplication).
|
|
901
|
+
Protocol handlers deduplicate for MCP wire format.
|
|
902
|
+
"""
|
|
903
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
904
|
+
if run_middleware:
|
|
905
|
+
mw_context = MiddlewareContext(
|
|
906
|
+
message={},
|
|
907
|
+
source="client",
|
|
908
|
+
type="request",
|
|
909
|
+
method="prompts/list",
|
|
910
|
+
fastmcp_context=ctx,
|
|
1415
911
|
)
|
|
1416
|
-
|
|
1417
|
-
|
|
912
|
+
return await self._run_middleware(
|
|
913
|
+
context=mw_context,
|
|
914
|
+
call_next=lambda context: self.list_prompts(run_middleware=False),
|
|
1418
915
|
)
|
|
1419
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1420
|
-
raise
|
|
1421
|
-
continue
|
|
1422
|
-
|
|
1423
|
-
return list(all_templates.values())
|
|
1424
916
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
917
|
+
# Get all prompts, apply session transforms, then filter enabled
|
|
918
|
+
prompts = list(await super().list_prompts())
|
|
919
|
+
prompts = await apply_session_transforms(prompts)
|
|
920
|
+
prompts = [p for p in prompts if is_enabled(p)]
|
|
921
|
+
|
|
922
|
+
skip_auth, token = _get_auth_context()
|
|
923
|
+
authorized: list[Prompt] = []
|
|
924
|
+
for prompt in prompts:
|
|
925
|
+
if not skip_auth and prompt.auth is not None:
|
|
926
|
+
ctx = AuthContext(token=token, component=prompt)
|
|
927
|
+
try:
|
|
928
|
+
if not run_auth_checks(prompt.auth, ctx):
|
|
929
|
+
continue
|
|
930
|
+
except AuthorizationError:
|
|
931
|
+
continue
|
|
932
|
+
authorized.append(prompt)
|
|
933
|
+
return authorized
|
|
1441
934
|
|
|
1442
|
-
async def
|
|
1443
|
-
|
|
1444
|
-
|
|
935
|
+
async def _get_prompt(
|
|
936
|
+
self, name: str, version: VersionSpec | None = None
|
|
937
|
+
) -> Prompt | None:
|
|
938
|
+
"""Get a prompt by name via aggregation from providers.
|
|
1445
939
|
|
|
1446
|
-
|
|
940
|
+
Extends AggregateProvider._get_prompt() with component-level auth checks.
|
|
1447
941
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
message=mcp.types.ListPromptsRequest(method="prompts/list"),
|
|
1452
|
-
source="client",
|
|
1453
|
-
type="request",
|
|
1454
|
-
method="prompts/list",
|
|
1455
|
-
fastmcp_context=fastmcp_ctx,
|
|
1456
|
-
)
|
|
1457
|
-
|
|
1458
|
-
# Apply the middleware chain.
|
|
1459
|
-
return list(
|
|
1460
|
-
await self._apply_middleware(
|
|
1461
|
-
context=mw_context, call_next=self._list_prompts
|
|
1462
|
-
)
|
|
1463
|
-
)
|
|
942
|
+
Args:
|
|
943
|
+
name: The prompt name.
|
|
944
|
+
version: Version filter (None returns highest version).
|
|
1464
945
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
1468
|
-
) -> list[Prompt]:
|
|
1469
|
-
"""
|
|
1470
|
-
List all available prompts.
|
|
946
|
+
Returns:
|
|
947
|
+
The prompt if found and authorized, None if not found or unauthorized.
|
|
1471
948
|
"""
|
|
1472
|
-
#
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
for prompt in local_prompts.values()
|
|
1477
|
-
if self._should_enable_component(prompt)
|
|
1478
|
-
]
|
|
1479
|
-
|
|
1480
|
-
# 2. Get from mounted servers
|
|
1481
|
-
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1482
|
-
# Use a dict to implement "later wins" deduplication by key
|
|
1483
|
-
all_prompts: dict[str, Prompt] = {
|
|
1484
|
-
prompt.key: prompt for prompt in filtered_local
|
|
1485
|
-
}
|
|
949
|
+
# Get prompt from AggregateProvider (handles aggregation and namespacing)
|
|
950
|
+
prompt = await super()._get_prompt(name, version)
|
|
951
|
+
if prompt is None:
|
|
952
|
+
return None
|
|
1486
953
|
|
|
1487
|
-
|
|
954
|
+
# Component auth - return None if unauthorized (consistent with list filtering)
|
|
955
|
+
skip_auth, token = _get_auth_context()
|
|
956
|
+
if not skip_auth and prompt.auth is not None:
|
|
957
|
+
ctx = AuthContext(token=token, component=prompt)
|
|
1488
958
|
try:
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
continue
|
|
959
|
+
if not run_auth_checks(prompt.auth, ctx):
|
|
960
|
+
return None
|
|
961
|
+
except AuthorizationError:
|
|
962
|
+
return None
|
|
1494
963
|
|
|
1495
|
-
|
|
1496
|
-
if mounted.prefix:
|
|
1497
|
-
key = f"{mounted.prefix}_{prompt.key}"
|
|
1498
|
-
else:
|
|
1499
|
-
key = prompt.key
|
|
1500
|
-
|
|
1501
|
-
if key != prompt.key:
|
|
1502
|
-
prompt = prompt.model_copy(key=key)
|
|
1503
|
-
# Later mounted servers override earlier ones
|
|
1504
|
-
all_prompts[key] = prompt
|
|
1505
|
-
except Exception as e:
|
|
1506
|
-
server_name = getattr(
|
|
1507
|
-
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1508
|
-
)
|
|
1509
|
-
logger.warning(
|
|
1510
|
-
f"Failed to list prompts from mounted server {server_name!r}: {e}"
|
|
1511
|
-
)
|
|
1512
|
-
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1513
|
-
raise
|
|
1514
|
-
continue
|
|
964
|
+
return prompt
|
|
1515
965
|
|
|
1516
|
-
|
|
966
|
+
async def get_prompt(
|
|
967
|
+
self, name: str, version: VersionSpec | None = None
|
|
968
|
+
) -> Prompt | None:
|
|
969
|
+
"""Get a prompt by name, filtering disabled prompts.
|
|
1517
970
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
) -> (
|
|
1521
|
-
list[ContentBlock]
|
|
1522
|
-
| tuple[list[ContentBlock], dict[str, Any]]
|
|
1523
|
-
| mcp.types.CallToolResult
|
|
1524
|
-
):
|
|
1525
|
-
"""
|
|
1526
|
-
Handle MCP 'callTool' requests.
|
|
1527
|
-
|
|
1528
|
-
Detects SEP-1686 task metadata and routes to background execution if supported.
|
|
971
|
+
Overrides Provider.get_prompt() to add visibility filtering after all
|
|
972
|
+
transforms (including session-level) have been applied.
|
|
1529
973
|
|
|
1530
974
|
Args:
|
|
1531
|
-
|
|
1532
|
-
|
|
975
|
+
name: The prompt name.
|
|
976
|
+
version: Version filter (None returns highest version).
|
|
1533
977
|
|
|
1534
978
|
Returns:
|
|
1535
|
-
|
|
979
|
+
The prompt if found and enabled, None otherwise.
|
|
1536
980
|
"""
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
981
|
+
prompt = await super().get_prompt(name, version)
|
|
982
|
+
if prompt is None:
|
|
983
|
+
return None
|
|
1540
984
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
task_meta = ctx.experimental.task_metadata
|
|
1550
|
-
except (AttributeError, LookupError):
|
|
1551
|
-
# No request context available - proceed without task metadata
|
|
1552
|
-
pass
|
|
1553
|
-
|
|
1554
|
-
# Get tool from local manager, mounted servers, or proxy
|
|
1555
|
-
tool = await self._get_tool_with_task_config(key)
|
|
1556
|
-
if (
|
|
1557
|
-
tool
|
|
1558
|
-
and self._should_enable_component(tool)
|
|
1559
|
-
and hasattr(tool, "task_config")
|
|
1560
|
-
):
|
|
1561
|
-
task_mode = tool.task_config.mode # type: ignore[union-attr]
|
|
1562
|
-
|
|
1563
|
-
# Enforce mode="required" - must have task metadata
|
|
1564
|
-
if task_mode == "required" and not task_meta:
|
|
1565
|
-
raise McpError(
|
|
1566
|
-
ErrorData(
|
|
1567
|
-
code=METHOD_NOT_FOUND,
|
|
1568
|
-
message=f"Tool '{key}' requires task-augmented execution",
|
|
1569
|
-
)
|
|
1570
|
-
)
|
|
1571
|
-
|
|
1572
|
-
# Route to background if task metadata present and mode allows
|
|
1573
|
-
if task_meta and task_mode != "forbidden":
|
|
1574
|
-
# For FunctionTool, use Docket for background execution
|
|
1575
|
-
if isinstance(tool, FunctionTool):
|
|
1576
|
-
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
1577
|
-
return await handle_tool_as_task(
|
|
1578
|
-
self, key, arguments, task_meta_dict
|
|
1579
|
-
)
|
|
1580
|
-
# For ProxyTool/mounted tools, proceed with normal execution
|
|
1581
|
-
# They will forward task metadata to their backend
|
|
1582
|
-
|
|
1583
|
-
# Forbidden mode: task requested but mode="forbidden"
|
|
1584
|
-
# Return error result with returned_immediately=True
|
|
1585
|
-
if task_meta and task_mode == "forbidden":
|
|
1586
|
-
return mcp.types.CallToolResult(
|
|
1587
|
-
content=[
|
|
1588
|
-
mcp.types.TextContent(
|
|
1589
|
-
type="text",
|
|
1590
|
-
text=f"Tool '{key}' does not support task-augmented execution",
|
|
1591
|
-
)
|
|
1592
|
-
],
|
|
1593
|
-
isError=True,
|
|
1594
|
-
_meta={
|
|
1595
|
-
"modelcontextprotocol.io/task": {
|
|
1596
|
-
"returned_immediately": True
|
|
1597
|
-
}
|
|
1598
|
-
},
|
|
1599
|
-
)
|
|
1600
|
-
|
|
1601
|
-
# Synchronous execution (normal path)
|
|
1602
|
-
result = await self._call_tool_middleware(key, arguments)
|
|
1603
|
-
return result.to_mcp_result()
|
|
1604
|
-
except DisabledError as e:
|
|
1605
|
-
raise NotFoundError(f"Unknown tool: {key}") from e
|
|
1606
|
-
except NotFoundError as e:
|
|
1607
|
-
raise NotFoundError(f"Unknown tool: {key}") from e
|
|
1608
|
-
|
|
1609
|
-
async def _call_tool_middleware(
|
|
985
|
+
# Apply session transforms to single item
|
|
986
|
+
prompts = await apply_session_transforms([prompt])
|
|
987
|
+
if not prompts or not is_enabled(prompts[0]):
|
|
988
|
+
return None
|
|
989
|
+
return prompts[0]
|
|
990
|
+
|
|
991
|
+
@overload
|
|
992
|
+
async def call_tool(
|
|
1610
993
|
self,
|
|
1611
|
-
|
|
1612
|
-
arguments: dict[str, Any],
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
994
|
+
name: str,
|
|
995
|
+
arguments: dict[str, Any] | None = None,
|
|
996
|
+
*,
|
|
997
|
+
version: VersionSpec | None = None,
|
|
998
|
+
run_middleware: bool = True,
|
|
999
|
+
task_meta: None = None,
|
|
1000
|
+
) -> ToolResult: ...
|
|
1617
1001
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1002
|
+
@overload
|
|
1003
|
+
async def call_tool(
|
|
1004
|
+
self,
|
|
1005
|
+
name: str,
|
|
1006
|
+
arguments: dict[str, Any] | None = None,
|
|
1007
|
+
*,
|
|
1008
|
+
version: VersionSpec | None = None,
|
|
1009
|
+
run_middleware: bool = True,
|
|
1010
|
+
task_meta: TaskMeta,
|
|
1011
|
+
) -> mcp.types.CreateTaskResult: ...
|
|
1628
1012
|
|
|
1629
|
-
async def
|
|
1013
|
+
async def call_tool(
|
|
1630
1014
|
self,
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
for mounted in reversed(self._mounted_servers):
|
|
1640
|
-
try_name = tool_name
|
|
1641
|
-
|
|
1642
|
-
# First check if tool_name is an overridden name (reverse lookup)
|
|
1643
|
-
if mounted.tool_names:
|
|
1644
|
-
for orig_key, override_name in mounted.tool_names.items():
|
|
1645
|
-
if override_name == tool_name:
|
|
1646
|
-
try_name = orig_key
|
|
1647
|
-
break
|
|
1648
|
-
else:
|
|
1649
|
-
# Not an override, try standard prefix stripping
|
|
1650
|
-
if mounted.prefix:
|
|
1651
|
-
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1652
|
-
continue
|
|
1653
|
-
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1654
|
-
elif mounted.prefix:
|
|
1655
|
-
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1656
|
-
continue
|
|
1657
|
-
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1015
|
+
name: str,
|
|
1016
|
+
arguments: dict[str, Any] | None = None,
|
|
1017
|
+
*,
|
|
1018
|
+
version: VersionSpec | None = None,
|
|
1019
|
+
run_middleware: bool = True,
|
|
1020
|
+
task_meta: TaskMeta | None = None,
|
|
1021
|
+
) -> ToolResult | mcp.types.CreateTaskResult:
|
|
1022
|
+
"""Call a tool by name.
|
|
1658
1023
|
|
|
1659
|
-
|
|
1660
|
-
# First, get the tool to check if parent's filter allows it
|
|
1661
|
-
# Use get_tool() instead of _tool_manager.get_tool() to support
|
|
1662
|
-
# nested mounted servers (tools mounted more than 2 levels deep)
|
|
1663
|
-
tool = await mounted.server.get_tool(try_name)
|
|
1664
|
-
if not self._should_enable_component(tool):
|
|
1665
|
-
# Parent filter blocks this tool, continue searching
|
|
1666
|
-
continue
|
|
1667
|
-
|
|
1668
|
-
return await mounted.server._call_tool_middleware(
|
|
1669
|
-
try_name, context.message.arguments or {}
|
|
1670
|
-
)
|
|
1671
|
-
except NotFoundError:
|
|
1672
|
-
continue
|
|
1024
|
+
This is the public API for executing tools. By default, middleware is applied.
|
|
1673
1025
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1026
|
+
Args:
|
|
1027
|
+
name: The tool name
|
|
1028
|
+
arguments: Tool arguments (optional)
|
|
1029
|
+
version: Specific version to call. If None, calls highest version.
|
|
1030
|
+
run_middleware: If True (default), apply the middleware chain.
|
|
1031
|
+
Set to False when called from middleware to avoid re-applying.
|
|
1032
|
+
task_meta: If provided, execute as a background task and return
|
|
1033
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
1034
|
+
return ToolResult.
|
|
1683
1035
|
|
|
1684
|
-
|
|
1036
|
+
Returns:
|
|
1037
|
+
ToolResult when task_meta is None.
|
|
1038
|
+
CreateTaskResult when task_meta is provided.
|
|
1685
1039
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1040
|
+
Raises:
|
|
1041
|
+
NotFoundError: If tool not found or disabled
|
|
1042
|
+
ToolError: If tool execution fails
|
|
1043
|
+
ValidationError: If arguments fail validation
|
|
1044
|
+
"""
|
|
1045
|
+
# Note: fn_key enrichment happens here after finding the tool.
|
|
1046
|
+
# For mounted servers, the parent's provider sets fn_key to the
|
|
1047
|
+
# namespaced key before delegating, ensuring correct Docket routing.
|
|
1048
|
+
|
|
1049
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
1050
|
+
if run_middleware:
|
|
1051
|
+
mw_context = MiddlewareContext[CallToolRequestParams](
|
|
1052
|
+
message=mcp.types.CallToolRequestParams(
|
|
1053
|
+
name=name, arguments=arguments or {}
|
|
1054
|
+
),
|
|
1055
|
+
source="client",
|
|
1056
|
+
type="request",
|
|
1057
|
+
method="tools/call",
|
|
1058
|
+
fastmcp_context=ctx,
|
|
1059
|
+
)
|
|
1060
|
+
return await self._run_middleware(
|
|
1061
|
+
context=mw_context,
|
|
1062
|
+
call_next=lambda context: self.call_tool(
|
|
1063
|
+
context.message.name,
|
|
1064
|
+
context.message.arguments or {},
|
|
1065
|
+
version=version,
|
|
1066
|
+
run_middleware=False,
|
|
1067
|
+
task_meta=task_meta,
|
|
1068
|
+
),
|
|
1069
|
+
)
|
|
1689
1070
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1071
|
+
# Core logic: find and execute tool (providers queried in parallel)
|
|
1072
|
+
# Use get_tool to apply transforms and filter disabled
|
|
1073
|
+
with server_span(
|
|
1074
|
+
f"tools/call {name}", "tools/call", self.name, "tool", name
|
|
1075
|
+
) as span:
|
|
1076
|
+
tool = await self.get_tool(name, version=version)
|
|
1077
|
+
if tool is None:
|
|
1078
|
+
raise NotFoundError(f"Unknown tool: {name!r}")
|
|
1079
|
+
span.set_attributes(tool.get_span_attributes())
|
|
1080
|
+
if task_meta is not None and task_meta.fn_key is None:
|
|
1081
|
+
task_meta = replace(task_meta, fn_key=tool.key)
|
|
1082
|
+
try:
|
|
1083
|
+
return await tool._run(arguments or {}, task_meta=task_meta)
|
|
1084
|
+
except FastMCPError:
|
|
1085
|
+
logger.exception(f"Error calling tool {name!r}")
|
|
1086
|
+
raise
|
|
1087
|
+
except (ValidationError, PydanticValidationError):
|
|
1088
|
+
logger.exception(f"Error validating tool {name!r}")
|
|
1089
|
+
raise
|
|
1090
|
+
except Exception as e:
|
|
1091
|
+
logger.exception(f"Error calling tool {name!r}")
|
|
1092
|
+
if self._mask_error_details:
|
|
1093
|
+
raise ToolError(f"Error calling tool {name!r}") from e
|
|
1094
|
+
raise ToolError(f"Error calling tool {name!r}: {e}") from e
|
|
1693
1095
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
# Task routing handled by custom handler
|
|
1697
|
-
return list[ReadResourceContents](
|
|
1698
|
-
await self._read_resource_middleware(uri)
|
|
1699
|
-
)
|
|
1700
|
-
except DisabledError as e:
|
|
1701
|
-
# convert to NotFoundError to avoid leaking resource presence
|
|
1702
|
-
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
|
|
1703
|
-
except NotFoundError as e:
|
|
1704
|
-
# standardize NotFound message
|
|
1705
|
-
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
|
|
1706
|
-
|
|
1707
|
-
async def _read_resource_middleware(
|
|
1096
|
+
@overload
|
|
1097
|
+
async def read_resource(
|
|
1708
1098
|
self,
|
|
1709
|
-
uri:
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
# Convert string URI to AnyUrl if needed
|
|
1716
|
-
uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
|
|
1099
|
+
uri: str,
|
|
1100
|
+
*,
|
|
1101
|
+
version: VersionSpec | None = None,
|
|
1102
|
+
run_middleware: bool = True,
|
|
1103
|
+
task_meta: None = None,
|
|
1104
|
+
) -> ResourceResult: ...
|
|
1717
1105
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
context=mw_context, call_next=self._read_resource
|
|
1728
|
-
)
|
|
1729
|
-
)
|
|
1106
|
+
@overload
|
|
1107
|
+
async def read_resource(
|
|
1108
|
+
self,
|
|
1109
|
+
uri: str,
|
|
1110
|
+
*,
|
|
1111
|
+
version: VersionSpec | None = None,
|
|
1112
|
+
run_middleware: bool = True,
|
|
1113
|
+
task_meta: TaskMeta,
|
|
1114
|
+
) -> mcp.types.CreateTaskResult: ...
|
|
1730
1115
|
|
|
1731
|
-
async def
|
|
1116
|
+
async def read_resource(
|
|
1732
1117
|
self,
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
# Try mounted servers in reverse order (later wins)
|
|
1741
|
-
for mounted in reversed(self._mounted_servers):
|
|
1742
|
-
key = uri_str
|
|
1743
|
-
if mounted.prefix:
|
|
1744
|
-
if not has_resource_prefix(key, mounted.prefix):
|
|
1745
|
-
continue
|
|
1746
|
-
key = remove_resource_prefix(key, mounted.prefix)
|
|
1747
|
-
|
|
1748
|
-
# First, get the resource/template to check if parent's filter allows it
|
|
1749
|
-
# Use get_resource_or_template to support nested mounted servers
|
|
1750
|
-
# (resources/templates mounted more than 2 levels deep)
|
|
1751
|
-
resource = await mounted.server._get_resource_or_template_or_none(key)
|
|
1752
|
-
if resource is None:
|
|
1753
|
-
continue
|
|
1754
|
-
if not self._should_enable_component(resource):
|
|
1755
|
-
# Parent filter blocks this resource, continue searching
|
|
1756
|
-
continue
|
|
1757
|
-
try:
|
|
1758
|
-
result = list(await mounted.server._read_resource_middleware(key))
|
|
1759
|
-
return result
|
|
1760
|
-
except NotFoundError:
|
|
1761
|
-
continue
|
|
1118
|
+
uri: str,
|
|
1119
|
+
*,
|
|
1120
|
+
version: VersionSpec | None = None,
|
|
1121
|
+
run_middleware: bool = True,
|
|
1122
|
+
task_meta: TaskMeta | None = None,
|
|
1123
|
+
) -> ResourceResult | mcp.types.CreateTaskResult:
|
|
1124
|
+
"""Read a resource by URI.
|
|
1762
1125
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
resource = await self._resource_manager.get_resource(uri_str)
|
|
1766
|
-
if self._should_enable_component(resource):
|
|
1767
|
-
content = await self._resource_manager.read_resource(uri_str)
|
|
1768
|
-
return [
|
|
1769
|
-
ReadResourceContents(
|
|
1770
|
-
content=content,
|
|
1771
|
-
mime_type=resource.mime_type,
|
|
1772
|
-
)
|
|
1773
|
-
]
|
|
1774
|
-
except NotFoundError:
|
|
1775
|
-
pass
|
|
1126
|
+
This is the public API for reading resources. By default, middleware is applied.
|
|
1127
|
+
Checks concrete resources first, then templates.
|
|
1776
1128
|
|
|
1777
|
-
|
|
1129
|
+
Args:
|
|
1130
|
+
uri: The resource URI
|
|
1131
|
+
version: Specific version to read. If None, reads highest version.
|
|
1132
|
+
run_middleware: If True (default), apply the middleware chain.
|
|
1133
|
+
Set to False when called from middleware to avoid re-applying.
|
|
1134
|
+
task_meta: If provided, execute as a background task and return
|
|
1135
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
1136
|
+
return ResourceResult.
|
|
1778
1137
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
"""
|
|
1783
|
-
Handle MCP 'getPrompt' requests.
|
|
1138
|
+
Returns:
|
|
1139
|
+
ResourceResult when task_meta is None.
|
|
1140
|
+
CreateTaskResult when task_meta is provided.
|
|
1784
1141
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1142
|
+
Raises:
|
|
1143
|
+
NotFoundError: If resource not found or disabled
|
|
1144
|
+
ResourceError: If resource read fails
|
|
1145
|
+
"""
|
|
1146
|
+
# Note: fn_key enrichment happens here after finding the resource/template.
|
|
1147
|
+
# Resources and templates use different key formats:
|
|
1148
|
+
# - Resources use resource.key (derived from the concrete URI)
|
|
1149
|
+
# - Templates use template.key (the template pattern)
|
|
1150
|
+
# For mounted servers, the parent's provider sets fn_key to the
|
|
1151
|
+
# namespaced key before delegating, ensuring correct Docket routing.
|
|
1152
|
+
|
|
1153
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
1154
|
+
if run_middleware:
|
|
1155
|
+
uri_param = AnyUrl(uri)
|
|
1156
|
+
mw_context = MiddlewareContext(
|
|
1157
|
+
message=mcp.types.ReadResourceRequestParams(uri=uri_param),
|
|
1158
|
+
source="client",
|
|
1159
|
+
type="request",
|
|
1160
|
+
method="resources/read",
|
|
1161
|
+
fastmcp_context=ctx,
|
|
1162
|
+
)
|
|
1163
|
+
return await self._run_middleware(
|
|
1164
|
+
context=mw_context,
|
|
1165
|
+
call_next=lambda context: self.read_resource(
|
|
1166
|
+
str(context.message.uri),
|
|
1167
|
+
version=version,
|
|
1168
|
+
run_middleware=False,
|
|
1169
|
+
task_meta=task_meta,
|
|
1170
|
+
),
|
|
1171
|
+
)
|
|
1788
1172
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1173
|
+
# Core logic: find and read resource (providers queried in parallel)
|
|
1174
|
+
with server_span(
|
|
1175
|
+
f"resources/read {uri}",
|
|
1176
|
+
"resources/read",
|
|
1177
|
+
self.name,
|
|
1178
|
+
"resource",
|
|
1179
|
+
uri,
|
|
1180
|
+
resource_uri=uri,
|
|
1181
|
+
) as span:
|
|
1182
|
+
# Try concrete resources first (transforms + auth via _get_resource)
|
|
1183
|
+
resource = await self.get_resource(uri, version=version)
|
|
1184
|
+
if resource is not None:
|
|
1185
|
+
span.set_attributes(resource.get_span_attributes())
|
|
1186
|
+
if task_meta is not None and task_meta.fn_key is None:
|
|
1187
|
+
task_meta = replace(task_meta, fn_key=resource.key)
|
|
1188
|
+
try:
|
|
1189
|
+
return await resource._read(task_meta=task_meta)
|
|
1190
|
+
except (FastMCPError, McpError):
|
|
1191
|
+
logger.exception(f"Error reading resource {uri!r}")
|
|
1192
|
+
raise
|
|
1193
|
+
except Exception as e:
|
|
1194
|
+
logger.exception(f"Error reading resource {uri!r}")
|
|
1195
|
+
if self._mask_error_details:
|
|
1196
|
+
raise ResourceError(
|
|
1197
|
+
f"Error reading resource {uri!r}"
|
|
1198
|
+
) from e
|
|
1199
|
+
raise ResourceError(
|
|
1200
|
+
f"Error reading resource {uri!r}: {e}"
|
|
1201
|
+
) from e
|
|
1202
|
+
|
|
1203
|
+
# Try templates (transforms + auth via get_resource_template)
|
|
1204
|
+
template = await self.get_resource_template(uri, version=version)
|
|
1205
|
+
if template is None:
|
|
1206
|
+
if version is None:
|
|
1207
|
+
raise NotFoundError(f"Unknown resource: {uri!r}")
|
|
1208
|
+
raise NotFoundError(
|
|
1209
|
+
f"Unknown resource: {uri!r} version {version!r}"
|
|
1210
|
+
)
|
|
1211
|
+
span.set_attributes(template.get_span_attributes())
|
|
1212
|
+
params = template.matches(uri)
|
|
1213
|
+
assert params is not None
|
|
1214
|
+
if task_meta is not None and task_meta.fn_key is None:
|
|
1215
|
+
task_meta = replace(task_meta, fn_key=template.key)
|
|
1216
|
+
try:
|
|
1217
|
+
return await template._read(uri, params, task_meta=task_meta)
|
|
1218
|
+
except (FastMCPError, McpError):
|
|
1219
|
+
logger.exception(f"Error reading resource {uri!r}")
|
|
1220
|
+
raise
|
|
1221
|
+
except Exception as e:
|
|
1222
|
+
logger.exception(f"Error reading resource {uri!r}")
|
|
1223
|
+
if self._mask_error_details:
|
|
1224
|
+
raise ResourceError(f"Error reading resource {uri!r}") from e
|
|
1225
|
+
raise ResourceError(f"Error reading resource {uri!r}: {e}") from e
|
|
1792
1226
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
async def _get_prompt_middleware(
|
|
1805
|
-
self, name: str, arguments: dict[str, Any] | None = None
|
|
1806
|
-
) -> GetPromptResult:
|
|
1807
|
-
"""
|
|
1808
|
-
Applies this server's middleware and delegates the filtered call to the manager.
|
|
1809
|
-
"""
|
|
1227
|
+
@overload
|
|
1228
|
+
async def render_prompt(
|
|
1229
|
+
self,
|
|
1230
|
+
name: str,
|
|
1231
|
+
arguments: dict[str, Any] | None = None,
|
|
1232
|
+
*,
|
|
1233
|
+
version: VersionSpec | None = None,
|
|
1234
|
+
run_middleware: bool = True,
|
|
1235
|
+
task_meta: None = None,
|
|
1236
|
+
) -> PromptResult: ...
|
|
1810
1237
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1238
|
+
@overload
|
|
1239
|
+
async def render_prompt(
|
|
1240
|
+
self,
|
|
1241
|
+
name: str,
|
|
1242
|
+
arguments: dict[str, Any] | None = None,
|
|
1243
|
+
*,
|
|
1244
|
+
version: VersionSpec | None = None,
|
|
1245
|
+
run_middleware: bool = True,
|
|
1246
|
+
task_meta: TaskMeta,
|
|
1247
|
+
) -> mcp.types.CreateTaskResult: ...
|
|
1821
1248
|
|
|
1822
|
-
async def
|
|
1249
|
+
async def render_prompt(
|
|
1823
1250
|
self,
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if not name.startswith(f"{mounted.prefix}_"):
|
|
1833
|
-
continue
|
|
1834
|
-
try_name = name[len(mounted.prefix) + 1 :]
|
|
1251
|
+
name: str,
|
|
1252
|
+
arguments: dict[str, Any] | None = None,
|
|
1253
|
+
*,
|
|
1254
|
+
version: VersionSpec | None = None,
|
|
1255
|
+
run_middleware: bool = True,
|
|
1256
|
+
task_meta: TaskMeta | None = None,
|
|
1257
|
+
) -> PromptResult | mcp.types.CreateTaskResult:
|
|
1258
|
+
"""Render a prompt by name.
|
|
1835
1259
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
# Use get_prompt() instead of _prompt_manager.get_prompt() to support
|
|
1839
|
-
# nested mounted servers (prompts mounted more than 2 levels deep)
|
|
1840
|
-
prompt = await mounted.server.get_prompt(try_name)
|
|
1841
|
-
if not self._should_enable_component(prompt):
|
|
1842
|
-
# Parent filter blocks this prompt, continue searching
|
|
1843
|
-
continue
|
|
1844
|
-
return await mounted.server._get_prompt_middleware(
|
|
1845
|
-
try_name, context.message.arguments
|
|
1846
|
-
)
|
|
1847
|
-
except NotFoundError:
|
|
1848
|
-
continue
|
|
1260
|
+
This is the public API for rendering prompts. By default, middleware is applied.
|
|
1261
|
+
Use get_prompt() to retrieve the prompt definition without rendering.
|
|
1849
1262
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1263
|
+
Args:
|
|
1264
|
+
name: The prompt name
|
|
1265
|
+
arguments: Prompt arguments (optional)
|
|
1266
|
+
version: Specific version to render. If None, renders highest version.
|
|
1267
|
+
run_middleware: If True (default), apply the middleware chain.
|
|
1268
|
+
Set to False when called from middleware to avoid re-applying.
|
|
1269
|
+
task_meta: If provided, execute as a background task and return
|
|
1270
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
1271
|
+
return PromptResult.
|
|
1272
|
+
|
|
1273
|
+
Returns:
|
|
1274
|
+
PromptResult when task_meta is None.
|
|
1275
|
+
CreateTaskResult when task_meta is provided.
|
|
1276
|
+
|
|
1277
|
+
Raises:
|
|
1278
|
+
NotFoundError: If prompt not found or disabled
|
|
1279
|
+
PromptError: If prompt rendering fails
|
|
1280
|
+
"""
|
|
1281
|
+
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
|
|
1282
|
+
if run_middleware:
|
|
1283
|
+
mw_context = MiddlewareContext(
|
|
1284
|
+
message=mcp.types.GetPromptRequestParams(
|
|
1285
|
+
name=name, arguments=arguments
|
|
1286
|
+
),
|
|
1287
|
+
source="client",
|
|
1288
|
+
type="request",
|
|
1289
|
+
method="prompts/get",
|
|
1290
|
+
fastmcp_context=ctx,
|
|
1291
|
+
)
|
|
1292
|
+
return await self._run_middleware(
|
|
1293
|
+
context=mw_context,
|
|
1294
|
+
call_next=lambda context: self.render_prompt(
|
|
1295
|
+
context.message.name,
|
|
1296
|
+
context.message.arguments,
|
|
1297
|
+
version=version,
|
|
1298
|
+
run_middleware=False,
|
|
1299
|
+
task_meta=task_meta,
|
|
1300
|
+
),
|
|
1856
1301
|
)
|
|
1857
|
-
except NotFoundError:
|
|
1858
|
-
pass
|
|
1859
1302
|
|
|
1860
|
-
|
|
1303
|
+
# Core logic: find and render prompt (providers queried in parallel)
|
|
1304
|
+
# Use get_prompt to apply transforms and filter disabled
|
|
1305
|
+
with server_span(
|
|
1306
|
+
f"prompts/get {name}", "prompts/get", self.name, "prompt", name
|
|
1307
|
+
) as span:
|
|
1308
|
+
prompt = await self.get_prompt(name, version=version)
|
|
1309
|
+
if prompt is None:
|
|
1310
|
+
raise NotFoundError(f"Unknown prompt: {name!r}")
|
|
1311
|
+
span.set_attributes(prompt.get_span_attributes())
|
|
1312
|
+
if task_meta is not None and task_meta.fn_key is None:
|
|
1313
|
+
task_meta = replace(task_meta, fn_key=prompt.key)
|
|
1314
|
+
try:
|
|
1315
|
+
return await prompt._render(arguments, task_meta=task_meta)
|
|
1316
|
+
except (FastMCPError, McpError):
|
|
1317
|
+
logger.exception(f"Error rendering prompt {name!r}")
|
|
1318
|
+
raise
|
|
1319
|
+
except Exception as e:
|
|
1320
|
+
logger.exception(f"Error rendering prompt {name!r}")
|
|
1321
|
+
if self._mask_error_details:
|
|
1322
|
+
raise PromptError(f"Error rendering prompt {name!r}") from e
|
|
1323
|
+
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
|
|
1861
1324
|
|
|
1862
|
-
def add_tool(self, tool: Tool) -> Tool:
|
|
1325
|
+
def add_tool(self, tool: Tool | Callable[..., Any]) -> Tool:
|
|
1863
1326
|
"""Add a tool to the server.
|
|
1864
1327
|
|
|
1865
1328
|
The tool function can optionally request a Context object by adding a parameter
|
|
1866
1329
|
with the Context type annotation. See the @tool decorator for examples.
|
|
1867
1330
|
|
|
1868
1331
|
Args:
|
|
1869
|
-
tool: The Tool instance to register
|
|
1332
|
+
tool: The Tool instance or @tool-decorated function to register
|
|
1870
1333
|
|
|
1871
1334
|
Returns:
|
|
1872
1335
|
The tool instance that was added to the server.
|
|
1873
1336
|
"""
|
|
1874
|
-
self.
|
|
1875
|
-
|
|
1876
|
-
# Send notification if we're in a request context
|
|
1877
|
-
try:
|
|
1878
|
-
from fastmcp.server.dependencies import get_context
|
|
1879
|
-
|
|
1880
|
-
context = get_context()
|
|
1881
|
-
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
1882
|
-
except RuntimeError:
|
|
1883
|
-
pass # No context available
|
|
1337
|
+
return self._local_provider.add_tool(tool)
|
|
1884
1338
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
def remove_tool(self, name: str) -> None:
|
|
1888
|
-
"""Remove a tool from the server.
|
|
1339
|
+
def remove_tool(self, name: str, version: str | None = None) -> None:
|
|
1340
|
+
"""Remove tool(s) from the server.
|
|
1889
1341
|
|
|
1890
1342
|
Args:
|
|
1891
|
-
name: The name of the tool to remove
|
|
1343
|
+
name: The name of the tool to remove.
|
|
1344
|
+
version: If None, removes ALL versions. If specified, removes only that version.
|
|
1892
1345
|
|
|
1893
1346
|
Raises:
|
|
1894
|
-
NotFoundError: If
|
|
1347
|
+
NotFoundError: If no matching tool is found.
|
|
1895
1348
|
"""
|
|
1896
|
-
self._tool_manager.remove_tool(name)
|
|
1897
|
-
|
|
1898
|
-
# Send notification if we're in a request context
|
|
1899
1349
|
try:
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
def add_tool_transformation(
|
|
1908
|
-
self, tool_name: str, transformation: ToolTransformConfig
|
|
1909
|
-
) -> None:
|
|
1910
|
-
"""Add a tool transformation."""
|
|
1911
|
-
self._tool_manager.add_tool_transformation(tool_name, transformation)
|
|
1912
|
-
|
|
1913
|
-
def remove_tool_transformation(self, tool_name: str) -> None:
|
|
1914
|
-
"""Remove a tool transformation."""
|
|
1915
|
-
self._tool_manager.remove_tool_transformation(tool_name)
|
|
1350
|
+
self._local_provider.remove_tool(name, version)
|
|
1351
|
+
except KeyError:
|
|
1352
|
+
if version is None:
|
|
1353
|
+
raise NotFoundError(f"Tool {name!r} not found") from None
|
|
1354
|
+
raise NotFoundError(
|
|
1355
|
+
f"Tool {name!r} version {version!r} not found"
|
|
1356
|
+
) from None
|
|
1916
1357
|
|
|
1917
1358
|
@overload
|
|
1918
1359
|
def tool(
|
|
@@ -1920,6 +1361,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1920
1361
|
name_or_fn: AnyFunction,
|
|
1921
1362
|
*,
|
|
1922
1363
|
name: str | None = None,
|
|
1364
|
+
version: str | int | None = None,
|
|
1923
1365
|
title: str | None = None,
|
|
1924
1366
|
description: str | None = None,
|
|
1925
1367
|
icons: list[mcp.types.Icon] | None = None,
|
|
@@ -1928,8 +1370,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1928
1370
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
1929
1371
|
exclude_args: list[str] | None = None,
|
|
1930
1372
|
meta: dict[str, Any] | None = None,
|
|
1931
|
-
enabled: bool | None = None,
|
|
1932
1373
|
task: bool | TaskConfig | None = None,
|
|
1374
|
+
timeout: float | None = None,
|
|
1375
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
1933
1376
|
) -> FunctionTool: ...
|
|
1934
1377
|
|
|
1935
1378
|
@overload
|
|
@@ -1938,6 +1381,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1938
1381
|
name_or_fn: str | None = None,
|
|
1939
1382
|
*,
|
|
1940
1383
|
name: str | None = None,
|
|
1384
|
+
version: str | int | None = None,
|
|
1941
1385
|
title: str | None = None,
|
|
1942
1386
|
description: str | None = None,
|
|
1943
1387
|
icons: list[mcp.types.Icon] | None = None,
|
|
@@ -1946,8 +1390,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1946
1390
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
1947
1391
|
exclude_args: list[str] | None = None,
|
|
1948
1392
|
meta: dict[str, Any] | None = None,
|
|
1949
|
-
enabled: bool | None = None,
|
|
1950
1393
|
task: bool | TaskConfig | None = None,
|
|
1394
|
+
timeout: float | None = None,
|
|
1395
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
1951
1396
|
) -> Callable[[AnyFunction], FunctionTool]: ...
|
|
1952
1397
|
|
|
1953
1398
|
def tool(
|
|
@@ -1955,6 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1955
1400
|
name_or_fn: str | AnyFunction | None = None,
|
|
1956
1401
|
*,
|
|
1957
1402
|
name: str | None = None,
|
|
1403
|
+
version: str | int | None = None,
|
|
1958
1404
|
title: str | None = None,
|
|
1959
1405
|
description: str | None = None,
|
|
1960
1406
|
icons: list[mcp.types.Icon] | None = None,
|
|
@@ -1963,9 +1409,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1963
1409
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
1964
1410
|
exclude_args: list[str] | None = None,
|
|
1965
1411
|
meta: dict[str, Any] | None = None,
|
|
1966
|
-
enabled: bool | None = None,
|
|
1967
1412
|
task: bool | TaskConfig | None = None,
|
|
1968
|
-
|
|
1413
|
+
timeout: float | None = None,
|
|
1414
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
1415
|
+
) -> (
|
|
1416
|
+
Callable[[AnyFunction], FunctionTool]
|
|
1417
|
+
| FunctionTool
|
|
1418
|
+
| partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
|
|
1419
|
+
):
|
|
1969
1420
|
"""Decorator to register a tool.
|
|
1970
1421
|
|
|
1971
1422
|
Tools can optionally request a Context object by adding a parameter with the
|
|
@@ -1989,7 +1440,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1989
1440
|
exclude_args: Optional list of argument names to exclude from the tool schema.
|
|
1990
1441
|
Deprecated: Use `Depends()` for dependency injection instead.
|
|
1991
1442
|
meta: Optional meta information about the tool
|
|
1992
|
-
enabled: Optional boolean to enable or disable the tool
|
|
1993
1443
|
|
|
1994
1444
|
Examples:
|
|
1995
1445
|
Register a tool with a custom name:
|
|
@@ -2015,73 +1465,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2015
1465
|
server.tool(my_function, name="custom_name")
|
|
2016
1466
|
```
|
|
2017
1467
|
"""
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
inspect.cleandoc(
|
|
2024
|
-
"""
|
|
2025
|
-
To decorate a classmethod, first define the method and then call
|
|
2026
|
-
tool() directly on the method instead of using it as a
|
|
2027
|
-
decorator. See https://gofastmcp.com/patterns/decorating-methods
|
|
2028
|
-
for examples and more information.
|
|
2029
|
-
"""
|
|
2030
|
-
)
|
|
2031
|
-
)
|
|
2032
|
-
|
|
2033
|
-
# Determine the actual name and function based on the calling pattern
|
|
2034
|
-
if inspect.isroutine(name_or_fn):
|
|
2035
|
-
# Case 1: @tool (without parens) - function passed directly
|
|
2036
|
-
# Case 2: direct call like tool(fn, name="something")
|
|
2037
|
-
fn = name_or_fn
|
|
2038
|
-
tool_name = name # Use keyword name if provided, otherwise None
|
|
2039
|
-
|
|
2040
|
-
# Resolve task parameter
|
|
2041
|
-
supports_task: bool | TaskConfig = (
|
|
2042
|
-
task if task is not None else self._support_tasks_by_default
|
|
2043
|
-
)
|
|
2044
|
-
|
|
2045
|
-
# Register the tool immediately and return the tool object
|
|
2046
|
-
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
|
2047
|
-
tool = Tool.from_function(
|
|
2048
|
-
fn,
|
|
2049
|
-
name=tool_name,
|
|
2050
|
-
title=title,
|
|
2051
|
-
description=description,
|
|
2052
|
-
icons=icons,
|
|
2053
|
-
tags=tags,
|
|
2054
|
-
output_schema=output_schema,
|
|
2055
|
-
annotations=annotations,
|
|
2056
|
-
exclude_args=exclude_args,
|
|
2057
|
-
meta=meta,
|
|
2058
|
-
serializer=self._tool_serializer,
|
|
2059
|
-
enabled=enabled,
|
|
2060
|
-
task=supports_task,
|
|
2061
|
-
)
|
|
2062
|
-
self.add_tool(tool)
|
|
2063
|
-
return tool
|
|
2064
|
-
|
|
2065
|
-
elif isinstance(name_or_fn, str):
|
|
2066
|
-
# Case 3: @tool("custom_name") - name passed as first argument
|
|
2067
|
-
if name is not None:
|
|
2068
|
-
raise TypeError(
|
|
2069
|
-
"Cannot specify both a name as first argument and as keyword argument. "
|
|
2070
|
-
f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both."
|
|
2071
|
-
)
|
|
2072
|
-
tool_name = name_or_fn
|
|
2073
|
-
elif name_or_fn is None:
|
|
2074
|
-
# Case 4: @tool or @tool(name="something") - use keyword name
|
|
2075
|
-
tool_name = name
|
|
2076
|
-
else:
|
|
2077
|
-
raise TypeError(
|
|
2078
|
-
f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}"
|
|
2079
|
-
)
|
|
2080
|
-
|
|
2081
|
-
# Return partial for cases where we need to wait for the function
|
|
2082
|
-
return partial(
|
|
2083
|
-
self.tool,
|
|
2084
|
-
name=tool_name,
|
|
1468
|
+
# Delegate to LocalProvider with server-level defaults
|
|
1469
|
+
result = self._local_provider.tool(
|
|
1470
|
+
name_or_fn,
|
|
1471
|
+
name=name,
|
|
1472
|
+
version=version,
|
|
2085
1473
|
title=title,
|
|
2086
1474
|
description=description,
|
|
2087
1475
|
icons=icons,
|
|
@@ -2090,31 +1478,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2090
1478
|
annotations=annotations,
|
|
2091
1479
|
exclude_args=exclude_args,
|
|
2092
1480
|
meta=meta,
|
|
2093
|
-
|
|
2094
|
-
|
|
1481
|
+
task=task if task is not None else self._support_tasks_by_default,
|
|
1482
|
+
timeout=timeout,
|
|
1483
|
+
serializer=self._tool_serializer,
|
|
1484
|
+
auth=auth,
|
|
2095
1485
|
)
|
|
2096
1486
|
|
|
2097
|
-
|
|
1487
|
+
return result
|
|
1488
|
+
|
|
1489
|
+
def add_resource(
|
|
1490
|
+
self, resource: Resource | Callable[..., Any]
|
|
1491
|
+
) -> Resource | ResourceTemplate:
|
|
2098
1492
|
"""Add a resource to the server.
|
|
2099
1493
|
|
|
2100
1494
|
Args:
|
|
2101
|
-
resource: A Resource instance to add
|
|
1495
|
+
resource: A Resource instance or @resource-decorated function to add
|
|
2102
1496
|
|
|
2103
1497
|
Returns:
|
|
2104
1498
|
The resource instance that was added to the server.
|
|
2105
1499
|
"""
|
|
2106
|
-
self.
|
|
2107
|
-
|
|
2108
|
-
# Send notification if we're in a request context
|
|
2109
|
-
try:
|
|
2110
|
-
from fastmcp.server.dependencies import get_context
|
|
2111
|
-
|
|
2112
|
-
context = get_context()
|
|
2113
|
-
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
2114
|
-
except RuntimeError:
|
|
2115
|
-
pass # No context available
|
|
2116
|
-
|
|
2117
|
-
return resource
|
|
1500
|
+
return self._local_provider.add_resource(resource)
|
|
2118
1501
|
|
|
2119
1502
|
def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
|
|
2120
1503
|
"""Add a resource template to the server.
|
|
@@ -2125,34 +1508,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2125
1508
|
Returns:
|
|
2126
1509
|
The template instance that was added to the server.
|
|
2127
1510
|
"""
|
|
2128
|
-
self.
|
|
2129
|
-
|
|
2130
|
-
# Send notification if we're in a request context
|
|
2131
|
-
try:
|
|
2132
|
-
from fastmcp.server.dependencies import get_context
|
|
2133
|
-
|
|
2134
|
-
context = get_context()
|
|
2135
|
-
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
2136
|
-
except RuntimeError:
|
|
2137
|
-
pass # No context available
|
|
2138
|
-
|
|
2139
|
-
return template
|
|
1511
|
+
return self._local_provider.add_template(template)
|
|
2140
1512
|
|
|
2141
1513
|
def resource(
|
|
2142
1514
|
self,
|
|
2143
1515
|
uri: str,
|
|
2144
1516
|
*,
|
|
2145
1517
|
name: str | None = None,
|
|
1518
|
+
version: str | int | None = None,
|
|
2146
1519
|
title: str | None = None,
|
|
2147
1520
|
description: str | None = None,
|
|
2148
1521
|
icons: list[mcp.types.Icon] | None = None,
|
|
2149
1522
|
mime_type: str | None = None,
|
|
2150
1523
|
tags: set[str] | None = None,
|
|
2151
|
-
enabled: bool | None = None,
|
|
2152
1524
|
annotations: Annotations | dict[str, Any] | None = None,
|
|
2153
1525
|
meta: dict[str, Any] | None = None,
|
|
2154
1526
|
task: bool | TaskConfig | None = None,
|
|
2155
|
-
|
|
1527
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
1528
|
+
) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]:
|
|
2156
1529
|
"""Decorator to register a function as a resource.
|
|
2157
1530
|
|
|
2158
1531
|
The function will be called when the resource is read to generate its content.
|
|
@@ -2174,7 +1547,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2174
1547
|
description: Optional description of the resource
|
|
2175
1548
|
mime_type: Optional MIME type for the resource
|
|
2176
1549
|
tags: Optional set of tags for categorizing the resource
|
|
2177
|
-
enabled: Optional boolean to enable or disable the resource
|
|
2178
1550
|
annotations: Optional annotations about the resource's behavior
|
|
2179
1551
|
meta: Optional meta information about the resource
|
|
2180
1552
|
|
|
@@ -2205,105 +1577,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2205
1577
|
return f"Weather for {city}: {data}"
|
|
2206
1578
|
```
|
|
2207
1579
|
"""
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
To decorate a classmethod, first define the method and then call
|
|
2224
|
-
resource() directly on the method instead of using it as a
|
|
2225
|
-
decorator. See https://gofastmcp.com/patterns/decorating-methods
|
|
2226
|
-
for examples and more information.
|
|
2227
|
-
"""
|
|
2228
|
-
)
|
|
2229
|
-
)
|
|
2230
|
-
|
|
2231
|
-
# Resolve task parameter
|
|
2232
|
-
supports_task: bool | TaskConfig = (
|
|
2233
|
-
task if task is not None else self._support_tasks_by_default
|
|
2234
|
-
)
|
|
1580
|
+
# Delegate to LocalProvider with server-level defaults
|
|
1581
|
+
inner_decorator = self._local_provider.resource(
|
|
1582
|
+
uri,
|
|
1583
|
+
name=name,
|
|
1584
|
+
version=version,
|
|
1585
|
+
title=title,
|
|
1586
|
+
description=description,
|
|
1587
|
+
icons=icons,
|
|
1588
|
+
mime_type=mime_type,
|
|
1589
|
+
tags=tags,
|
|
1590
|
+
annotations=annotations,
|
|
1591
|
+
meta=meta,
|
|
1592
|
+
task=task if task is not None else self._support_tasks_by_default,
|
|
1593
|
+
auth=auth,
|
|
1594
|
+
)
|
|
2235
1595
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
# Use wrapper to check for user-facing parameters
|
|
2239
|
-
from fastmcp.server.dependencies import without_injected_parameters
|
|
2240
|
-
|
|
2241
|
-
wrapper_fn = without_injected_parameters(fn)
|
|
2242
|
-
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
|
|
2243
|
-
|
|
2244
|
-
if has_uri_params or has_func_params:
|
|
2245
|
-
template = ResourceTemplate.from_function(
|
|
2246
|
-
fn=fn,
|
|
2247
|
-
uri_template=uri,
|
|
2248
|
-
name=name,
|
|
2249
|
-
title=title,
|
|
2250
|
-
description=description,
|
|
2251
|
-
icons=icons,
|
|
2252
|
-
mime_type=mime_type,
|
|
2253
|
-
tags=tags,
|
|
2254
|
-
enabled=enabled,
|
|
2255
|
-
annotations=annotations,
|
|
2256
|
-
meta=meta,
|
|
2257
|
-
task=supports_task,
|
|
2258
|
-
)
|
|
2259
|
-
self.add_template(template)
|
|
2260
|
-
return template
|
|
2261
|
-
elif not has_uri_params and not has_func_params:
|
|
2262
|
-
resource = Resource.from_function(
|
|
2263
|
-
fn=fn,
|
|
2264
|
-
uri=uri,
|
|
2265
|
-
name=name,
|
|
2266
|
-
title=title,
|
|
2267
|
-
description=description,
|
|
2268
|
-
icons=icons,
|
|
2269
|
-
mime_type=mime_type,
|
|
2270
|
-
tags=tags,
|
|
2271
|
-
enabled=enabled,
|
|
2272
|
-
annotations=annotations,
|
|
2273
|
-
meta=meta,
|
|
2274
|
-
task=supports_task,
|
|
2275
|
-
)
|
|
2276
|
-
self.add_resource(resource)
|
|
2277
|
-
return resource
|
|
2278
|
-
else:
|
|
2279
|
-
raise ValueError(
|
|
2280
|
-
"Invalid resource or template definition due to a "
|
|
2281
|
-
"mismatch between URI parameters and function parameters."
|
|
2282
|
-
)
|
|
1596
|
+
def decorator(fn: AnyFunction) -> Resource | ResourceTemplate | AnyFunction:
|
|
1597
|
+
return inner_decorator(fn)
|
|
2283
1598
|
|
|
2284
1599
|
return decorator
|
|
2285
1600
|
|
|
2286
|
-
def add_prompt(self, prompt: Prompt) -> Prompt:
|
|
1601
|
+
def add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt:
|
|
2287
1602
|
"""Add a prompt to the server.
|
|
2288
1603
|
|
|
2289
1604
|
Args:
|
|
2290
|
-
prompt: A Prompt instance to add
|
|
1605
|
+
prompt: A Prompt instance or @prompt-decorated function to add
|
|
2291
1606
|
|
|
2292
1607
|
Returns:
|
|
2293
1608
|
The prompt instance that was added to the server.
|
|
2294
1609
|
"""
|
|
2295
|
-
self.
|
|
2296
|
-
|
|
2297
|
-
# Send notification if we're in a request context
|
|
2298
|
-
try:
|
|
2299
|
-
from fastmcp.server.dependencies import get_context
|
|
2300
|
-
|
|
2301
|
-
context = get_context()
|
|
2302
|
-
context._queue_prompt_list_changed() # type: ignore[private-use]
|
|
2303
|
-
except RuntimeError:
|
|
2304
|
-
pass # No context available
|
|
2305
|
-
|
|
2306
|
-
return prompt
|
|
1610
|
+
return self._local_provider.add_prompt(prompt)
|
|
2307
1611
|
|
|
2308
1612
|
@overload
|
|
2309
1613
|
def prompt(
|
|
@@ -2311,13 +1615,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2311
1615
|
name_or_fn: AnyFunction,
|
|
2312
1616
|
*,
|
|
2313
1617
|
name: str | None = None,
|
|
1618
|
+
version: str | int | None = None,
|
|
2314
1619
|
title: str | None = None,
|
|
2315
1620
|
description: str | None = None,
|
|
2316
1621
|
icons: list[mcp.types.Icon] | None = None,
|
|
2317
1622
|
tags: set[str] | None = None,
|
|
2318
|
-
enabled: bool | None = None,
|
|
2319
1623
|
meta: dict[str, Any] | None = None,
|
|
2320
1624
|
task: bool | TaskConfig | None = None,
|
|
1625
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
2321
1626
|
) -> FunctionPrompt: ...
|
|
2322
1627
|
|
|
2323
1628
|
@overload
|
|
@@ -2326,13 +1631,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2326
1631
|
name_or_fn: str | None = None,
|
|
2327
1632
|
*,
|
|
2328
1633
|
name: str | None = None,
|
|
1634
|
+
version: str | int | None = None,
|
|
2329
1635
|
title: str | None = None,
|
|
2330
1636
|
description: str | None = None,
|
|
2331
1637
|
icons: list[mcp.types.Icon] | None = None,
|
|
2332
1638
|
tags: set[str] | None = None,
|
|
2333
|
-
enabled: bool | None = None,
|
|
2334
1639
|
meta: dict[str, Any] | None = None,
|
|
2335
1640
|
task: bool | TaskConfig | None = None,
|
|
1641
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
2336
1642
|
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
|
2337
1643
|
|
|
2338
1644
|
def prompt(
|
|
@@ -2340,14 +1646,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2340
1646
|
name_or_fn: str | AnyFunction | None = None,
|
|
2341
1647
|
*,
|
|
2342
1648
|
name: str | None = None,
|
|
1649
|
+
version: str | int | None = None,
|
|
2343
1650
|
title: str | None = None,
|
|
2344
1651
|
description: str | None = None,
|
|
2345
1652
|
icons: list[mcp.types.Icon] | None = None,
|
|
2346
1653
|
tags: set[str] | None = None,
|
|
2347
|
-
enabled: bool | None = None,
|
|
2348
1654
|
meta: dict[str, Any] | None = None,
|
|
2349
1655
|
task: bool | TaskConfig | None = None,
|
|
2350
|
-
|
|
1656
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
1657
|
+
) -> (
|
|
1658
|
+
Callable[[AnyFunction], FunctionPrompt]
|
|
1659
|
+
| FunctionPrompt
|
|
1660
|
+
| partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
|
|
1661
|
+
):
|
|
2351
1662
|
"""Decorator to register a prompt.
|
|
2352
1663
|
|
|
2353
1664
|
Prompts can optionally request a Context object by adding a parameter with the
|
|
@@ -2366,7 +1677,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2366
1677
|
name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
|
|
2367
1678
|
description: Optional description of what the prompt does
|
|
2368
1679
|
tags: Optional set of tags for categorizing the prompt
|
|
2369
|
-
enabled: Optional boolean to enable or disable the prompt
|
|
2370
1680
|
meta: Optional meta information about the prompt
|
|
2371
1681
|
|
|
2372
1682
|
Examples:
|
|
@@ -2417,241 +1727,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2417
1727
|
server.prompt(my_function, name="custom_name")
|
|
2418
1728
|
```
|
|
2419
1729
|
"""
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
To decorate a classmethod, first define the method and then call
|
|
2426
|
-
prompt() directly on the method instead of using it as a
|
|
2427
|
-
decorator. See https://gofastmcp.com/patterns/decorating-methods
|
|
2428
|
-
for examples and more information.
|
|
2429
|
-
"""
|
|
2430
|
-
)
|
|
2431
|
-
)
|
|
2432
|
-
|
|
2433
|
-
# Determine the actual name and function based on the calling pattern
|
|
2434
|
-
if inspect.isroutine(name_or_fn):
|
|
2435
|
-
# Case 1: @prompt (without parens) - function passed directly as decorator
|
|
2436
|
-
# Case 2: direct call like prompt(fn, name="something")
|
|
2437
|
-
fn = name_or_fn
|
|
2438
|
-
prompt_name = name # Use keyword name if provided, otherwise None
|
|
2439
|
-
|
|
2440
|
-
# Resolve task parameter
|
|
2441
|
-
supports_task: bool | TaskConfig = (
|
|
2442
|
-
task if task is not None else self._support_tasks_by_default
|
|
2443
|
-
)
|
|
2444
|
-
|
|
2445
|
-
# Register the prompt immediately
|
|
2446
|
-
prompt = Prompt.from_function(
|
|
2447
|
-
fn=fn,
|
|
2448
|
-
name=prompt_name,
|
|
2449
|
-
title=title,
|
|
2450
|
-
description=description,
|
|
2451
|
-
icons=icons,
|
|
2452
|
-
tags=tags,
|
|
2453
|
-
enabled=enabled,
|
|
2454
|
-
meta=meta,
|
|
2455
|
-
task=supports_task,
|
|
2456
|
-
)
|
|
2457
|
-
self.add_prompt(prompt)
|
|
2458
|
-
|
|
2459
|
-
return prompt
|
|
2460
|
-
|
|
2461
|
-
elif isinstance(name_or_fn, str):
|
|
2462
|
-
# Case 3: @prompt("custom_name") - name passed as first argument
|
|
2463
|
-
if name is not None:
|
|
2464
|
-
raise TypeError(
|
|
2465
|
-
"Cannot specify both a name as first argument and as keyword argument. "
|
|
2466
|
-
f"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both."
|
|
2467
|
-
)
|
|
2468
|
-
prompt_name = name_or_fn
|
|
2469
|
-
elif name_or_fn is None:
|
|
2470
|
-
# Case 4: @prompt() or @prompt(name="something") - use keyword name
|
|
2471
|
-
prompt_name = name
|
|
2472
|
-
else:
|
|
2473
|
-
raise TypeError(
|
|
2474
|
-
f"First argument to @prompt must be a function, string, or None, got {type(name_or_fn)}"
|
|
2475
|
-
)
|
|
2476
|
-
|
|
2477
|
-
# Return partial for cases where we need to wait for the function
|
|
2478
|
-
return partial(
|
|
2479
|
-
self.prompt,
|
|
2480
|
-
name=prompt_name,
|
|
1730
|
+
# Delegate to LocalProvider with server-level defaults
|
|
1731
|
+
return self._local_provider.prompt(
|
|
1732
|
+
name_or_fn,
|
|
1733
|
+
name=name,
|
|
1734
|
+
version=version,
|
|
2481
1735
|
title=title,
|
|
2482
1736
|
description=description,
|
|
2483
1737
|
icons=icons,
|
|
2484
1738
|
tags=tags,
|
|
2485
|
-
enabled=enabled,
|
|
2486
1739
|
meta=meta,
|
|
2487
|
-
task=task,
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
async def run_stdio_async(
|
|
2491
|
-
self, show_banner: bool = True, log_level: str | None = None
|
|
2492
|
-
) -> None:
|
|
2493
|
-
"""Run the server using stdio transport.
|
|
2494
|
-
|
|
2495
|
-
Args:
|
|
2496
|
-
show_banner: Whether to display the server banner
|
|
2497
|
-
log_level: Log level for the server
|
|
2498
|
-
"""
|
|
2499
|
-
# Display server banner
|
|
2500
|
-
if show_banner:
|
|
2501
|
-
log_server_banner(server=self)
|
|
2502
|
-
|
|
2503
|
-
with temporary_log_level(log_level):
|
|
2504
|
-
async with self._lifespan_manager():
|
|
2505
|
-
async with stdio_server() as (read_stream, write_stream):
|
|
2506
|
-
logger.info(
|
|
2507
|
-
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
2508
|
-
)
|
|
2509
|
-
|
|
2510
|
-
await self._mcp_server.run(
|
|
2511
|
-
read_stream,
|
|
2512
|
-
write_stream,
|
|
2513
|
-
self._mcp_server.create_initialization_options(
|
|
2514
|
-
notification_options=NotificationOptions(
|
|
2515
|
-
tools_changed=True
|
|
2516
|
-
),
|
|
2517
|
-
),
|
|
2518
|
-
)
|
|
2519
|
-
|
|
2520
|
-
async def run_http_async(
|
|
2521
|
-
self,
|
|
2522
|
-
show_banner: bool = True,
|
|
2523
|
-
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
2524
|
-
host: str | None = None,
|
|
2525
|
-
port: int | None = None,
|
|
2526
|
-
log_level: str | None = None,
|
|
2527
|
-
path: str | None = None,
|
|
2528
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
2529
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
2530
|
-
json_response: bool | None = None,
|
|
2531
|
-
stateless_http: bool | None = None,
|
|
2532
|
-
) -> None:
|
|
2533
|
-
"""Run the server using HTTP transport.
|
|
2534
|
-
|
|
2535
|
-
Args:
|
|
2536
|
-
transport: Transport protocol to use - either "streamable-http" (default) or "sse"
|
|
2537
|
-
host: Host address to bind to (defaults to settings.host)
|
|
2538
|
-
port: Port to bind to (defaults to settings.port)
|
|
2539
|
-
log_level: Log level for the server (defaults to settings.log_level)
|
|
2540
|
-
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
2541
|
-
uvicorn_config: Additional configuration for the Uvicorn server
|
|
2542
|
-
middleware: A list of middleware to apply to the app
|
|
2543
|
-
json_response: Whether to use JSON response format (defaults to settings.json_response)
|
|
2544
|
-
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
|
2545
|
-
"""
|
|
2546
|
-
host = host or self._deprecated_settings.host
|
|
2547
|
-
port = port or self._deprecated_settings.port
|
|
2548
|
-
default_log_level_to_use = (
|
|
2549
|
-
log_level or self._deprecated_settings.log_level
|
|
2550
|
-
).lower()
|
|
2551
|
-
|
|
2552
|
-
app = self.http_app(
|
|
2553
|
-
path=path,
|
|
2554
|
-
transport=transport,
|
|
2555
|
-
middleware=middleware,
|
|
2556
|
-
json_response=json_response,
|
|
2557
|
-
stateless_http=stateless_http,
|
|
1740
|
+
task=task if task is not None else self._support_tasks_by_default,
|
|
1741
|
+
auth=auth,
|
|
2558
1742
|
)
|
|
2559
1743
|
|
|
2560
|
-
# Display server banner
|
|
2561
|
-
if show_banner:
|
|
2562
|
-
log_server_banner(server=self)
|
|
2563
|
-
uvicorn_config_from_user = uvicorn_config or {}
|
|
2564
|
-
|
|
2565
|
-
config_kwargs: dict[str, Any] = {
|
|
2566
|
-
"timeout_graceful_shutdown": 0,
|
|
2567
|
-
"lifespan": "on",
|
|
2568
|
-
"ws": "websockets-sansio",
|
|
2569
|
-
}
|
|
2570
|
-
config_kwargs.update(uvicorn_config_from_user)
|
|
2571
|
-
|
|
2572
|
-
if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
|
|
2573
|
-
config_kwargs["log_level"] = default_log_level_to_use
|
|
2574
|
-
|
|
2575
|
-
with temporary_log_level(log_level):
|
|
2576
|
-
async with self._lifespan_manager():
|
|
2577
|
-
config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
|
|
2578
|
-
server = uvicorn.Server(config)
|
|
2579
|
-
path = app.state.path.lstrip("/") # type: ignore
|
|
2580
|
-
logger.info(
|
|
2581
|
-
f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
|
|
2582
|
-
)
|
|
2583
|
-
|
|
2584
|
-
await server.serve()
|
|
2585
|
-
|
|
2586
|
-
def http_app(
|
|
2587
|
-
self,
|
|
2588
|
-
path: str | None = None,
|
|
2589
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
2590
|
-
json_response: bool | None = None,
|
|
2591
|
-
stateless_http: bool | None = None,
|
|
2592
|
-
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
2593
|
-
event_store: EventStore | None = None,
|
|
2594
|
-
retry_interval: int | None = None,
|
|
2595
|
-
) -> StarletteWithLifespan:
|
|
2596
|
-
"""Create a Starlette app using the specified HTTP transport.
|
|
2597
|
-
|
|
2598
|
-
Args:
|
|
2599
|
-
path: The path for the HTTP endpoint
|
|
2600
|
-
middleware: A list of middleware to apply to the app
|
|
2601
|
-
json_response: Whether to use JSON response format
|
|
2602
|
-
stateless_http: Whether to use stateless mode (new transport per request)
|
|
2603
|
-
transport: Transport protocol to use - "http", "streamable-http", or "sse"
|
|
2604
|
-
event_store: Optional event store for SSE polling/resumability. When set,
|
|
2605
|
-
enables clients to reconnect and resume receiving events after
|
|
2606
|
-
server-initiated disconnections. Only used with streamable-http transport.
|
|
2607
|
-
retry_interval: Optional retry interval in milliseconds for SSE polling.
|
|
2608
|
-
Controls how quickly clients should reconnect after server-initiated
|
|
2609
|
-
disconnections. Requires event_store to be set. Only used with
|
|
2610
|
-
streamable-http transport.
|
|
2611
|
-
|
|
2612
|
-
Returns:
|
|
2613
|
-
A Starlette application configured with the specified transport
|
|
2614
|
-
"""
|
|
2615
|
-
|
|
2616
|
-
if transport in ("streamable-http", "http"):
|
|
2617
|
-
return create_streamable_http_app(
|
|
2618
|
-
server=self,
|
|
2619
|
-
streamable_http_path=path
|
|
2620
|
-
or self._deprecated_settings.streamable_http_path,
|
|
2621
|
-
event_store=event_store,
|
|
2622
|
-
retry_interval=retry_interval,
|
|
2623
|
-
auth=self.auth,
|
|
2624
|
-
json_response=(
|
|
2625
|
-
json_response
|
|
2626
|
-
if json_response is not None
|
|
2627
|
-
else self._deprecated_settings.json_response
|
|
2628
|
-
),
|
|
2629
|
-
stateless_http=(
|
|
2630
|
-
stateless_http
|
|
2631
|
-
if stateless_http is not None
|
|
2632
|
-
else self._deprecated_settings.stateless_http
|
|
2633
|
-
),
|
|
2634
|
-
debug=self._deprecated_settings.debug,
|
|
2635
|
-
middleware=middleware,
|
|
2636
|
-
)
|
|
2637
|
-
elif transport == "sse":
|
|
2638
|
-
return create_sse_app(
|
|
2639
|
-
server=self,
|
|
2640
|
-
message_path=self._deprecated_settings.message_path,
|
|
2641
|
-
sse_path=path or self._deprecated_settings.sse_path,
|
|
2642
|
-
auth=self.auth,
|
|
2643
|
-
debug=self._deprecated_settings.debug,
|
|
2644
|
-
middleware=middleware,
|
|
2645
|
-
)
|
|
2646
|
-
|
|
2647
1744
|
def mount(
|
|
2648
1745
|
self,
|
|
2649
1746
|
server: FastMCP[LifespanResultT],
|
|
2650
|
-
|
|
1747
|
+
namespace: str | None = None,
|
|
2651
1748
|
as_proxy: bool | None = None,
|
|
2652
1749
|
tool_names: dict[str, str] | None = None,
|
|
1750
|
+
prefix: str | None = None, # deprecated, use namespace
|
|
2653
1751
|
) -> None:
|
|
2654
|
-
"""Mount another FastMCP server on this server with an optional
|
|
1752
|
+
"""Mount another FastMCP server on this server with an optional namespace.
|
|
2655
1753
|
|
|
2656
1754
|
Unlike importing (with import_server), mounting establishes a dynamic connection
|
|
2657
1755
|
between servers. When a client interacts with a mounted server's objects through
|
|
@@ -2659,67 +1757,83 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2659
1757
|
This means changes to the mounted server are immediately reflected when accessed
|
|
2660
1758
|
through the parent.
|
|
2661
1759
|
|
|
2662
|
-
When a server is mounted with a
|
|
2663
|
-
- Tools from the mounted server are accessible with
|
|
2664
|
-
Example: If server has a tool named "get_weather", it will be available as "
|
|
2665
|
-
- Resources are accessible with
|
|
1760
|
+
When a server is mounted with a namespace:
|
|
1761
|
+
- Tools from the mounted server are accessible with namespaced names.
|
|
1762
|
+
Example: If server has a tool named "get_weather", it will be available as "namespace_get_weather".
|
|
1763
|
+
- Resources are accessible with namespaced URIs.
|
|
2666
1764
|
Example: If server has a resource with URI "weather://forecast", it will be available as
|
|
2667
|
-
"weather://
|
|
2668
|
-
- Templates are accessible with
|
|
1765
|
+
"weather://namespace/forecast".
|
|
1766
|
+
- Templates are accessible with namespaced URI templates.
|
|
2669
1767
|
Example: If server has a template with URI "weather://location/{id}", it will be available
|
|
2670
|
-
as "weather://
|
|
2671
|
-
- Prompts are accessible with
|
|
1768
|
+
as "weather://namespace/location/{id}".
|
|
1769
|
+
- Prompts are accessible with namespaced names.
|
|
2672
1770
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
2673
|
-
"
|
|
1771
|
+
"namespace_weather_prompt".
|
|
2674
1772
|
|
|
2675
|
-
When a server is mounted without a
|
|
1773
|
+
When a server is mounted without a namespace (namespace=None), its tools, resources, templates,
|
|
2676
1774
|
and prompts are accessible with their original names. Multiple servers can be mounted
|
|
2677
|
-
without
|
|
1775
|
+
without namespaces, and they will be tried in order until a match is found.
|
|
2678
1776
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
directly accesses the mounted server's objects in-memory for better performance.
|
|
2682
|
-
In this mode, no client lifecycle events occur on the mounted server, including
|
|
2683
|
-
lifespan execution.
|
|
2684
|
-
|
|
2685
|
-
2. Proxy mounting (default when server has a custom lifespan): The parent server
|
|
2686
|
-
treats the mounted server as a separate entity and communicates with it via a
|
|
2687
|
-
Client transport. This preserves all client-facing behaviors, including lifespan
|
|
2688
|
-
execution, but with slightly higher overhead.
|
|
1777
|
+
The mounted server's lifespan is executed when the parent server starts, and its
|
|
1778
|
+
middleware chain is invoked for all operations (tool calls, resource reads, prompts).
|
|
2689
1779
|
|
|
2690
1780
|
Args:
|
|
2691
1781
|
server: The FastMCP server to mount.
|
|
2692
|
-
|
|
1782
|
+
namespace: Optional namespace to use for the mounted server's objects. If None,
|
|
2693
1783
|
the server's objects are accessible with their original names.
|
|
2694
|
-
as_proxy:
|
|
2695
|
-
|
|
2696
|
-
|
|
1784
|
+
as_proxy: Deprecated. Mounted servers now always have their lifespan and
|
|
1785
|
+
middleware invoked. To create a proxy server, use create_proxy()
|
|
1786
|
+
explicitly before mounting.
|
|
2697
1787
|
tool_names: Optional mapping of original tool names to custom names. Use this
|
|
2698
|
-
to override
|
|
1788
|
+
to override namespaced names. Keys are the original tool names from the
|
|
2699
1789
|
mounted server.
|
|
1790
|
+
prefix: Deprecated. Use namespace instead.
|
|
2700
1791
|
"""
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
1792
|
+
import warnings
|
|
1793
|
+
|
|
1794
|
+
from fastmcp.server.providers.fastmcp_provider import FastMCPProvider
|
|
1795
|
+
|
|
1796
|
+
# Handle deprecated prefix parameter
|
|
1797
|
+
if prefix is not None:
|
|
1798
|
+
warnings.warn(
|
|
1799
|
+
"The 'prefix' parameter is deprecated, use 'namespace' instead",
|
|
1800
|
+
DeprecationWarning,
|
|
1801
|
+
stacklevel=2,
|
|
1802
|
+
)
|
|
1803
|
+
if namespace is None:
|
|
1804
|
+
namespace = prefix
|
|
1805
|
+
else:
|
|
1806
|
+
raise ValueError("Cannot specify both 'prefix' and 'namespace'")
|
|
1807
|
+
|
|
1808
|
+
if as_proxy is not None:
|
|
1809
|
+
warnings.warn(
|
|
1810
|
+
"as_proxy is deprecated and will be removed in a future version. "
|
|
1811
|
+
"Mounted servers now always have their lifespan and middleware invoked. "
|
|
1812
|
+
"To create a proxy server, use create_proxy() explicitly.",
|
|
1813
|
+
DeprecationWarning,
|
|
1814
|
+
stacklevel=2,
|
|
1815
|
+
)
|
|
1816
|
+
# Still honor the flag for backward compatibility
|
|
1817
|
+
if as_proxy:
|
|
1818
|
+
from fastmcp.server.providers.proxy import FastMCPProxy
|
|
1819
|
+
|
|
1820
|
+
if not isinstance(server, FastMCPProxy):
|
|
1821
|
+
server = FastMCP.as_proxy(server)
|
|
1822
|
+
|
|
1823
|
+
# Create provider and add it with namespace
|
|
1824
|
+
provider: Provider = FastMCPProvider(server)
|
|
1825
|
+
|
|
1826
|
+
# Apply tool renames first (scoped to this provider), then namespace
|
|
1827
|
+
# So foo → bar with namespace="baz" becomes baz_bar
|
|
1828
|
+
if tool_names:
|
|
1829
|
+
transforms = {
|
|
1830
|
+
old_name: ToolTransformConfig(name=new_name)
|
|
1831
|
+
for old_name, new_name in tool_names.items()
|
|
1832
|
+
}
|
|
1833
|
+
provider = provider.wrap_transform(ToolTransform(transforms))
|
|
1834
|
+
|
|
1835
|
+
# Use add_provider with namespace (applies namespace in AggregateProvider)
|
|
1836
|
+
self.add_provider(provider, namespace=namespace or "")
|
|
2723
1837
|
|
|
2724
1838
|
async def import_server(
|
|
2725
1839
|
self,
|
|
@@ -2730,6 +1844,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2730
1844
|
Import the MCP objects from another FastMCP server into this one,
|
|
2731
1845
|
optionally with a given prefix.
|
|
2732
1846
|
|
|
1847
|
+
.. deprecated::
|
|
1848
|
+
Use :meth:`mount` instead. ``import_server`` will be removed in a
|
|
1849
|
+
future version.
|
|
1850
|
+
|
|
2733
1851
|
Note that when a server is *imported*, its objects are immediately
|
|
2734
1852
|
registered to the importing server. This is a one-time operation and
|
|
2735
1853
|
future changes to the imported server will not be reflected in the
|
|
@@ -2757,34 +1875,48 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2757
1875
|
prefix: Optional prefix to use for the imported server's objects. If None,
|
|
2758
1876
|
objects are imported with their original names.
|
|
2759
1877
|
"""
|
|
1878
|
+
import warnings
|
|
1879
|
+
|
|
1880
|
+
warnings.warn(
|
|
1881
|
+
"import_server is deprecated, use mount() instead",
|
|
1882
|
+
DeprecationWarning,
|
|
1883
|
+
stacklevel=2,
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
1887
|
+
"""Add prefix to resource URI: protocol://path → protocol://prefix/path."""
|
|
1888
|
+
match = URI_PATTERN.match(uri)
|
|
1889
|
+
if match:
|
|
1890
|
+
protocol, path = match.groups()
|
|
1891
|
+
return f"{protocol}{prefix}/{path}"
|
|
1892
|
+
return uri
|
|
1893
|
+
|
|
2760
1894
|
# Import tools from the server
|
|
2761
|
-
for
|
|
1895
|
+
for tool in await server.list_tools():
|
|
2762
1896
|
if prefix:
|
|
2763
|
-
tool = tool.model_copy(
|
|
2764
|
-
self.
|
|
1897
|
+
tool = tool.model_copy(update={"name": f"{prefix}_{tool.name}"})
|
|
1898
|
+
self.add_tool(tool)
|
|
2765
1899
|
|
|
2766
1900
|
# Import resources and templates from the server
|
|
2767
|
-
for
|
|
1901
|
+
for resource in await server.list_resources():
|
|
2768
1902
|
if prefix:
|
|
2769
|
-
|
|
2770
|
-
resource = resource.model_copy(
|
|
2771
|
-
|
|
2772
|
-
)
|
|
2773
|
-
self._resource_manager.add_resource(resource)
|
|
1903
|
+
new_uri = add_resource_prefix(str(resource.uri), prefix)
|
|
1904
|
+
resource = resource.model_copy(update={"uri": new_uri})
|
|
1905
|
+
self.add_resource(resource)
|
|
2774
1906
|
|
|
2775
|
-
for
|
|
1907
|
+
for template in await server.list_resource_templates():
|
|
2776
1908
|
if prefix:
|
|
2777
|
-
|
|
1909
|
+
new_uri_template = add_resource_prefix(template.uri_template, prefix)
|
|
2778
1910
|
template = template.model_copy(
|
|
2779
|
-
update={"
|
|
1911
|
+
update={"uri_template": new_uri_template}
|
|
2780
1912
|
)
|
|
2781
|
-
self.
|
|
1913
|
+
self.add_template(template)
|
|
2782
1914
|
|
|
2783
1915
|
# Import prompts from the server
|
|
2784
|
-
for
|
|
1916
|
+
for prompt in await server.list_prompts():
|
|
2785
1917
|
if prefix:
|
|
2786
|
-
prompt = prompt.model_copy(
|
|
2787
|
-
self.
|
|
1918
|
+
prompt = prompt.model_copy(update={"name": f"{prefix}_{prompt.name}"})
|
|
1919
|
+
self.add_prompt(prompt)
|
|
2788
1920
|
|
|
2789
1921
|
if server._lifespan != default_lifespan:
|
|
2790
1922
|
from warnings import warn
|
|
@@ -2807,19 +1939,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2807
1939
|
cls,
|
|
2808
1940
|
openapi_spec: dict[str, Any],
|
|
2809
1941
|
client: httpx.AsyncClient,
|
|
1942
|
+
name: str = "OpenAPI Server",
|
|
2810
1943
|
route_maps: list[RouteMap] | None = None,
|
|
2811
1944
|
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2812
1945
|
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
2813
1946
|
mcp_names: dict[str, str] | None = None,
|
|
2814
1947
|
tags: set[str] | None = None,
|
|
1948
|
+
timeout: float | None = None,
|
|
2815
1949
|
**settings: Any,
|
|
2816
|
-
) ->
|
|
1950
|
+
) -> Self:
|
|
2817
1951
|
"""
|
|
2818
1952
|
Create a FastMCP server from an OpenAPI specification.
|
|
1953
|
+
|
|
1954
|
+
Args:
|
|
1955
|
+
openapi_spec: OpenAPI schema as a dictionary
|
|
1956
|
+
client: httpx AsyncClient for making HTTP requests
|
|
1957
|
+
name: Name for the MCP server
|
|
1958
|
+
route_maps: Optional list of RouteMap objects defining route mappings
|
|
1959
|
+
route_map_fn: Optional callable for advanced route type mapping
|
|
1960
|
+
mcp_component_fn: Optional callable for component customization
|
|
1961
|
+
mcp_names: Optional dictionary mapping operationId to component names
|
|
1962
|
+
tags: Optional set of tags to add to all components
|
|
1963
|
+
timeout: Optional timeout (in seconds) for all requests
|
|
1964
|
+
**settings: Additional settings passed to FastMCP
|
|
1965
|
+
|
|
1966
|
+
Returns:
|
|
1967
|
+
A FastMCP server with an OpenAPIProvider attached.
|
|
2819
1968
|
"""
|
|
2820
|
-
from .openapi import
|
|
1969
|
+
from .providers.openapi import OpenAPIProvider
|
|
2821
1970
|
|
|
2822
|
-
|
|
1971
|
+
provider: Provider = OpenAPIProvider(
|
|
2823
1972
|
openapi_spec=openapi_spec,
|
|
2824
1973
|
client=client,
|
|
2825
1974
|
route_maps=route_maps,
|
|
@@ -2827,8 +1976,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2827
1976
|
mcp_component_fn=mcp_component_fn,
|
|
2828
1977
|
mcp_names=mcp_names,
|
|
2829
1978
|
tags=tags,
|
|
2830
|
-
|
|
1979
|
+
timeout=timeout,
|
|
2831
1980
|
)
|
|
1981
|
+
return cls(name=name, providers=[provider], **settings)
|
|
2832
1982
|
|
|
2833
1983
|
@classmethod
|
|
2834
1984
|
def from_fastapi(
|
|
@@ -2841,12 +1991,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2841
1991
|
mcp_names: dict[str, str] | None = None,
|
|
2842
1992
|
httpx_client_kwargs: dict[str, Any] | None = None,
|
|
2843
1993
|
tags: set[str] | None = None,
|
|
1994
|
+
timeout: float | None = None,
|
|
2844
1995
|
**settings: Any,
|
|
2845
|
-
) ->
|
|
1996
|
+
) -> Self:
|
|
2846
1997
|
"""
|
|
2847
1998
|
Create a FastMCP server from a FastAPI application.
|
|
1999
|
+
|
|
2000
|
+
Args:
|
|
2001
|
+
app: FastAPI application instance
|
|
2002
|
+
name: Name for the MCP server (defaults to app.title)
|
|
2003
|
+
route_maps: Optional list of RouteMap objects defining route mappings
|
|
2004
|
+
route_map_fn: Optional callable for advanced route type mapping
|
|
2005
|
+
mcp_component_fn: Optional callable for component customization
|
|
2006
|
+
mcp_names: Optional dictionary mapping operationId to component names
|
|
2007
|
+
httpx_client_kwargs: Optional kwargs passed to httpx.AsyncClient
|
|
2008
|
+
tags: Optional set of tags to add to all components
|
|
2009
|
+
timeout: Optional timeout (in seconds) for all requests
|
|
2010
|
+
**settings: Additional settings passed to FastMCP
|
|
2011
|
+
|
|
2012
|
+
Returns:
|
|
2013
|
+
A FastMCP server with an OpenAPIProvider attached.
|
|
2848
2014
|
"""
|
|
2849
|
-
from .openapi import
|
|
2015
|
+
from .providers.openapi import OpenAPIProvider
|
|
2850
2016
|
|
|
2851
2017
|
if httpx_client_kwargs is None:
|
|
2852
2018
|
httpx_client_kwargs = {}
|
|
@@ -2857,19 +2023,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2857
2023
|
**httpx_client_kwargs,
|
|
2858
2024
|
)
|
|
2859
2025
|
|
|
2860
|
-
|
|
2026
|
+
server_name = name or app.title
|
|
2861
2027
|
|
|
2862
|
-
|
|
2028
|
+
provider: Provider = OpenAPIProvider(
|
|
2863
2029
|
openapi_spec=app.openapi(),
|
|
2864
2030
|
client=client,
|
|
2865
|
-
name=name,
|
|
2866
2031
|
route_maps=route_maps,
|
|
2867
2032
|
route_map_fn=route_map_fn,
|
|
2868
2033
|
mcp_component_fn=mcp_component_fn,
|
|
2869
2034
|
mcp_names=mcp_names,
|
|
2870
2035
|
tags=tags,
|
|
2871
|
-
|
|
2036
|
+
timeout=timeout,
|
|
2872
2037
|
)
|
|
2038
|
+
return cls(name=server_name, providers=[provider], **settings)
|
|
2873
2039
|
|
|
2874
2040
|
@classmethod
|
|
2875
2041
|
def as_proxy(
|
|
@@ -2889,79 +2055,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2889
2055
|
) -> FastMCPProxy:
|
|
2890
2056
|
"""Create a FastMCP proxy server for the given backend.
|
|
2891
2057
|
|
|
2058
|
+
.. deprecated::
|
|
2059
|
+
Use :func:`fastmcp.server.create_proxy` instead.
|
|
2060
|
+
This method will be removed in a future version.
|
|
2061
|
+
|
|
2892
2062
|
The `backend` argument can be either an existing `fastmcp.client.Client`
|
|
2893
2063
|
instance or any value accepted as the `transport` argument of
|
|
2894
2064
|
`fastmcp.client.Client`. This mirrors the convenience of the
|
|
2895
2065
|
`fastmcp.client.Client` constructor.
|
|
2896
2066
|
"""
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
proxy_logger = get_logger(__name__)
|
|
2907
|
-
proxy_logger.info(
|
|
2908
|
-
"Proxy detected connected client - reusing existing session for all requests. "
|
|
2909
|
-
"This may cause context mixing in concurrent scenarios."
|
|
2910
|
-
)
|
|
2911
|
-
|
|
2912
|
-
# Reuse sessions - return the same client instance
|
|
2913
|
-
def reuse_client_factory():
|
|
2914
|
-
return client
|
|
2915
|
-
|
|
2916
|
-
client_factory = reuse_client_factory
|
|
2917
|
-
else:
|
|
2918
|
-
# Fresh sessions per request
|
|
2919
|
-
def fresh_client_factory():
|
|
2920
|
-
return client.new()
|
|
2921
|
-
|
|
2922
|
-
client_factory = fresh_client_factory
|
|
2923
|
-
else:
|
|
2924
|
-
base_client = ProxyClient(backend) # type: ignore
|
|
2925
|
-
|
|
2926
|
-
# Fresh client created from transport - use fresh sessions per request
|
|
2927
|
-
def proxy_client_factory():
|
|
2928
|
-
return base_client.new()
|
|
2929
|
-
|
|
2930
|
-
client_factory = proxy_client_factory
|
|
2931
|
-
|
|
2932
|
-
return FastMCPProxy(client_factory=client_factory, **settings)
|
|
2933
|
-
|
|
2934
|
-
def _should_enable_component(
|
|
2935
|
-
self,
|
|
2936
|
-
component: FastMCPComponent,
|
|
2937
|
-
) -> bool:
|
|
2938
|
-
"""
|
|
2939
|
-
Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
|
|
2940
|
-
|
|
2941
|
-
Rules:
|
|
2942
|
-
- If the component's enabled property is False, always return False.
|
|
2943
|
-
- If both include_tags and exclude_tags are None, return True.
|
|
2944
|
-
- If exclude_tags is provided, check each exclude tag:
|
|
2945
|
-
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
|
2946
|
-
- If include_tags is provided, check each include tag:
|
|
2947
|
-
- If the include tag is a string, it must be present in the input tags to include.
|
|
2948
|
-
- If include_tags is provided and none of the include tags match, return False.
|
|
2949
|
-
- If include_tags is not provided, return True.
|
|
2950
|
-
"""
|
|
2951
|
-
if not component.enabled:
|
|
2952
|
-
return False
|
|
2953
|
-
|
|
2954
|
-
if self.include_tags is None and self.exclude_tags is None:
|
|
2955
|
-
return True
|
|
2956
|
-
|
|
2957
|
-
if self.exclude_tags is not None:
|
|
2958
|
-
if any(etag in component.tags for etag in self.exclude_tags):
|
|
2959
|
-
return False
|
|
2960
|
-
|
|
2961
|
-
if self.include_tags is not None:
|
|
2962
|
-
return bool(any(itag in component.tags for itag in self.include_tags))
|
|
2963
|
-
|
|
2964
|
-
return True
|
|
2067
|
+
if fastmcp.settings.deprecation_warnings:
|
|
2068
|
+
warnings.warn(
|
|
2069
|
+
"FastMCP.as_proxy() is deprecated. Use create_proxy() from "
|
|
2070
|
+
"fastmcp.server instead: `from fastmcp.server import create_proxy`",
|
|
2071
|
+
DeprecationWarning,
|
|
2072
|
+
stacklevel=2,
|
|
2073
|
+
)
|
|
2074
|
+
# Call the module-level create_proxy function directly
|
|
2075
|
+
return create_proxy(backend, **settings)
|
|
2965
2076
|
|
|
2966
2077
|
@classmethod
|
|
2967
2078
|
def generate_name(cls, name: str | None = None) -> str:
|
|
@@ -2973,129 +2084,61 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2973
2084
|
return f"{class_name}-{name}-{secrets.token_hex(2)}"
|
|
2974
2085
|
|
|
2975
2086
|
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
server: FastMCP[Any]
|
|
2980
|
-
tool_names: dict[str, str] | None = None
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
2984
|
-
"""Add a prefix to a resource URI using path formatting (resource://prefix/path).
|
|
2985
|
-
|
|
2986
|
-
Args:
|
|
2987
|
-
uri: The original resource URI
|
|
2988
|
-
prefix: The prefix to add
|
|
2989
|
-
|
|
2990
|
-
Returns:
|
|
2991
|
-
The resource URI with the prefix added
|
|
2992
|
-
|
|
2993
|
-
Examples:
|
|
2994
|
-
```python
|
|
2995
|
-
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2996
|
-
"resource://prefix/path/to/resource"
|
|
2997
|
-
```
|
|
2998
|
-
With absolute path:
|
|
2999
|
-
```python
|
|
3000
|
-
add_resource_prefix("resource:///absolute/path", "prefix")
|
|
3001
|
-
"resource://prefix//absolute/path"
|
|
3002
|
-
```
|
|
3003
|
-
|
|
3004
|
-
Raises:
|
|
3005
|
-
ValueError: If the URI doesn't match the expected protocol://path format
|
|
3006
|
-
"""
|
|
3007
|
-
if not prefix:
|
|
3008
|
-
return uri
|
|
3009
|
-
|
|
3010
|
-
# Split the URI into protocol and path
|
|
3011
|
-
match = URI_PATTERN.match(uri)
|
|
3012
|
-
if not match:
|
|
3013
|
-
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2087
|
+
# -----------------------------------------------------------------------------
|
|
2088
|
+
# Module-level Factory Functions
|
|
2089
|
+
# -----------------------------------------------------------------------------
|
|
3014
2090
|
|
|
3015
|
-
protocol, path = match.groups()
|
|
3016
2091
|
|
|
3017
|
-
|
|
3018
|
-
|
|
2092
|
+
def create_proxy(
|
|
2093
|
+
target: (
|
|
2094
|
+
Client[ClientTransportT]
|
|
2095
|
+
| ClientTransport
|
|
2096
|
+
| FastMCP[Any]
|
|
2097
|
+
| FastMCP1Server
|
|
2098
|
+
| AnyUrl
|
|
2099
|
+
| Path
|
|
2100
|
+
| MCPConfig
|
|
2101
|
+
| dict[str, Any]
|
|
2102
|
+
| str
|
|
2103
|
+
),
|
|
2104
|
+
**settings: Any,
|
|
2105
|
+
) -> FastMCPProxy:
|
|
2106
|
+
"""Create a FastMCP proxy server for the given target.
|
|
3019
2107
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
"""Remove a prefix from a resource URI.
|
|
2108
|
+
This is the recommended way to create a proxy server. For lower-level control,
|
|
2109
|
+
use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`.
|
|
3023
2110
|
|
|
3024
2111
|
Args:
|
|
3025
|
-
|
|
3026
|
-
|
|
2112
|
+
target: The backend to proxy to. Can be:
|
|
2113
|
+
- A Client instance (connected or disconnected)
|
|
2114
|
+
- A ClientTransport
|
|
2115
|
+
- A FastMCP server instance
|
|
2116
|
+
- A URL string or AnyUrl
|
|
2117
|
+
- A Path to a server script
|
|
2118
|
+
- An MCPConfig or dict
|
|
2119
|
+
**settings: Additional settings passed to FastMCPProxy (name, etc.)
|
|
3027
2120
|
|
|
3028
2121
|
Returns:
|
|
3029
|
-
|
|
2122
|
+
A FastMCPProxy server that proxies to the target.
|
|
3030
2123
|
|
|
3031
|
-
|
|
3032
|
-
```python
|
|
3033
|
-
remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
3034
|
-
"resource://path/to/resource"
|
|
3035
|
-
```
|
|
3036
|
-
With absolute path:
|
|
2124
|
+
Example:
|
|
3037
2125
|
```python
|
|
3038
|
-
|
|
3039
|
-
"resource:///absolute/path"
|
|
3040
|
-
```
|
|
3041
|
-
|
|
3042
|
-
Raises:
|
|
3043
|
-
ValueError: If the URI doesn't match the expected protocol://path format
|
|
3044
|
-
"""
|
|
3045
|
-
if not prefix:
|
|
3046
|
-
return uri
|
|
2126
|
+
from fastmcp.server import create_proxy
|
|
3047
2127
|
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
if not match:
|
|
3051
|
-
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2128
|
+
# Create a proxy to a remote server
|
|
2129
|
+
proxy = create_proxy("http://remote-server/mcp")
|
|
3052
2130
|
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
# Check if the path starts with the prefix followed by a /
|
|
3056
|
-
prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
|
|
3057
|
-
path_match = re.match(prefix_pattern, path)
|
|
3058
|
-
if not path_match:
|
|
3059
|
-
return uri
|
|
3060
|
-
|
|
3061
|
-
# Return the URI without the prefix
|
|
3062
|
-
return f"{protocol}{path_match.group(1)}"
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
def has_resource_prefix(uri: str, prefix: str) -> bool:
|
|
3066
|
-
"""Check if a resource URI has a specific prefix.
|
|
3067
|
-
|
|
3068
|
-
Args:
|
|
3069
|
-
uri: The resource URI to check
|
|
3070
|
-
prefix: The prefix to look for
|
|
3071
|
-
|
|
3072
|
-
Returns:
|
|
3073
|
-
True if the URI has the specified prefix, False otherwise
|
|
3074
|
-
|
|
3075
|
-
Examples:
|
|
3076
|
-
```python
|
|
3077
|
-
has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
3078
|
-
True
|
|
3079
|
-
```
|
|
3080
|
-
With other path:
|
|
3081
|
-
```python
|
|
3082
|
-
has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
3083
|
-
False
|
|
2131
|
+
# Create a proxy to another FastMCP server
|
|
2132
|
+
proxy = create_proxy(other_server)
|
|
3084
2133
|
```
|
|
3085
|
-
|
|
3086
|
-
Raises:
|
|
3087
|
-
ValueError: If the URI doesn't match the expected protocol://path format
|
|
3088
2134
|
"""
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
# Check if the path starts with the prefix followed by a /
|
|
3100
|
-
prefix_pattern = f"^{re.escape(prefix)}/"
|
|
3101
|
-
return bool(re.match(prefix_pattern, path))
|
|
2135
|
+
from fastmcp.server.providers.proxy import (
|
|
2136
|
+
FastMCPProxy,
|
|
2137
|
+
_create_client_factory,
|
|
2138
|
+
)
|
|
2139
|
+
|
|
2140
|
+
client_factory = _create_client_factory(target)
|
|
2141
|
+
return FastMCPProxy(
|
|
2142
|
+
client_factory=client_factory,
|
|
2143
|
+
**settings,
|
|
2144
|
+
)
|