fastmcp 2.12.1__py3-none-any.whl → 2.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -7,7 +7,14 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import secrets
|
|
9
9
|
import warnings
|
|
10
|
-
from collections.abc import
|
|
10
|
+
from collections.abc import (
|
|
11
|
+
AsyncIterator,
|
|
12
|
+
Awaitable,
|
|
13
|
+
Callable,
|
|
14
|
+
Collection,
|
|
15
|
+
Mapping,
|
|
16
|
+
Sequence,
|
|
17
|
+
)
|
|
11
18
|
from contextlib import (
|
|
12
19
|
AbstractAsyncContextManager,
|
|
13
20
|
AsyncExitStack,
|
|
@@ -47,9 +54,11 @@ import fastmcp
|
|
|
47
54
|
import fastmcp.server
|
|
48
55
|
from fastmcp.exceptions import DisabledError, NotFoundError
|
|
49
56
|
from fastmcp.mcp_config import MCPConfig
|
|
50
|
-
from fastmcp.prompts import Prompt
|
|
57
|
+
from fastmcp.prompts import Prompt
|
|
51
58
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
52
|
-
from fastmcp.
|
|
59
|
+
from fastmcp.prompts.prompt_manager import PromptManager
|
|
60
|
+
from fastmcp.resources.resource import Resource
|
|
61
|
+
from fastmcp.resources.resource_manager import ResourceManager
|
|
53
62
|
from fastmcp.resources.template import ResourceTemplate
|
|
54
63
|
from fastmcp.server.auth import AuthProvider
|
|
55
64
|
from fastmcp.server.http import (
|
|
@@ -60,17 +69,17 @@ from fastmcp.server.http import (
|
|
|
60
69
|
from fastmcp.server.low_level import LowLevelServer
|
|
61
70
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
62
71
|
from fastmcp.settings import Settings
|
|
63
|
-
from fastmcp.tools import ToolManager
|
|
64
72
|
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
73
|
+
from fastmcp.tools.tool_manager import ToolManager
|
|
65
74
|
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
66
75
|
from fastmcp.utilities.cli import log_server_banner
|
|
67
76
|
from fastmcp.utilities.components import FastMCPComponent
|
|
68
|
-
from fastmcp.utilities.logging import get_logger
|
|
77
|
+
from fastmcp.utilities.logging import get_logger, temporary_log_level
|
|
69
78
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
70
79
|
|
|
71
80
|
if TYPE_CHECKING:
|
|
72
81
|
from fastmcp.client import Client
|
|
73
|
-
from fastmcp.client.
|
|
82
|
+
from fastmcp.client.client import FastMCP1Server
|
|
74
83
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
75
84
|
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
76
85
|
from fastmcp.experimental.server.openapi.routing import (
|
|
@@ -84,6 +93,8 @@ if TYPE_CHECKING:
|
|
|
84
93
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
85
94
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
86
95
|
from fastmcp.server.proxy import FastMCPProxy
|
|
96
|
+
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
97
|
+
from fastmcp.tools.tool import ToolResultSerializerType
|
|
87
98
|
|
|
88
99
|
logger = get_logger(__name__)
|
|
89
100
|
|
|
@@ -93,6 +104,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
|
93
104
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
94
105
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
95
106
|
|
|
107
|
+
LifespanCallable = Callable[
|
|
108
|
+
["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
|
|
109
|
+
]
|
|
110
|
+
|
|
96
111
|
|
|
97
112
|
@asynccontextmanager
|
|
98
113
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -102,26 +117,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
|
|
|
102
117
|
server: The server instance this lifespan is managing
|
|
103
118
|
|
|
104
119
|
Returns:
|
|
105
|
-
An empty
|
|
120
|
+
An empty dictionary as the lifespan result.
|
|
106
121
|
"""
|
|
107
122
|
yield {}
|
|
108
123
|
|
|
109
124
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
lifespan: Callable[
|
|
113
|
-
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
114
|
-
],
|
|
125
|
+
def _lifespan_proxy(
|
|
126
|
+
fastmcp_server: FastMCP[LifespanResultT],
|
|
115
127
|
) -> Callable[
|
|
116
128
|
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
117
129
|
]:
|
|
118
130
|
@asynccontextmanager
|
|
119
131
|
async def wrap(
|
|
120
|
-
|
|
132
|
+
low_level_server: LowLevelServer[LifespanResultT],
|
|
121
133
|
) -> AsyncIterator[LifespanResultT]:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
if fastmcp_server._lifespan is default_lifespan:
|
|
135
|
+
yield {}
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if not fastmcp_server._lifespan_result_set:
|
|
139
|
+
raise RuntimeError(
|
|
140
|
+
"FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
|
|
141
|
+
+ " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
yield fastmcp_server._lifespan_result
|
|
125
145
|
|
|
126
146
|
return wrap
|
|
127
147
|
|
|
@@ -133,27 +153,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
133
153
|
instructions: str | None = None,
|
|
134
154
|
*,
|
|
135
155
|
version: str | None = None,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
AbstractAsyncContextManager[LifespanResultT],
|
|
142
|
-
]
|
|
143
|
-
| None
|
|
144
|
-
) = None,
|
|
156
|
+
website_url: str | None = None,
|
|
157
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
158
|
+
auth: AuthProvider | NotSetT | None = NotSet,
|
|
159
|
+
middleware: Sequence[Middleware] | None = None,
|
|
160
|
+
lifespan: LifespanCallable | None = None,
|
|
145
161
|
dependencies: list[str] | None = None,
|
|
146
162
|
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
147
163
|
mask_error_details: bool | None = None,
|
|
148
|
-
tools:
|
|
149
|
-
tool_transformations:
|
|
150
|
-
tool_serializer:
|
|
151
|
-
include_tags:
|
|
152
|
-
exclude_tags:
|
|
164
|
+
tools: Sequence[Tool | Callable[..., Any]] | None = None,
|
|
165
|
+
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
166
|
+
tool_serializer: ToolResultSerializerType | None = None,
|
|
167
|
+
include_tags: Collection[str] | None = None,
|
|
168
|
+
exclude_tags: Collection[str] | None = None,
|
|
153
169
|
include_fastmcp_meta: bool | None = None,
|
|
154
170
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
155
171
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
156
172
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
173
|
+
strict_input_validation: bool | None = None,
|
|
157
174
|
# ---
|
|
158
175
|
# ---
|
|
159
176
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -177,42 +194,46 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
177
194
|
|
|
178
195
|
self._additional_http_routes: list[BaseRoute] = []
|
|
179
196
|
self._mounted_servers: list[MountedServer] = []
|
|
180
|
-
self._tool_manager = ToolManager(
|
|
197
|
+
self._tool_manager: ToolManager = ToolManager(
|
|
181
198
|
duplicate_behavior=on_duplicate_tools,
|
|
182
199
|
mask_error_details=mask_error_details,
|
|
183
200
|
transformations=tool_transformations,
|
|
184
201
|
)
|
|
185
|
-
self._resource_manager = ResourceManager(
|
|
202
|
+
self._resource_manager: ResourceManager = ResourceManager(
|
|
186
203
|
duplicate_behavior=on_duplicate_resources,
|
|
187
204
|
mask_error_details=mask_error_details,
|
|
188
205
|
)
|
|
189
|
-
self._prompt_manager = PromptManager(
|
|
206
|
+
self._prompt_manager: PromptManager = PromptManager(
|
|
190
207
|
duplicate_behavior=on_duplicate_prompts,
|
|
191
208
|
mask_error_details=mask_error_details,
|
|
192
209
|
)
|
|
193
|
-
self._tool_serializer = tool_serializer
|
|
210
|
+
self._tool_serializer: Callable[[Any], str] | None = tool_serializer
|
|
211
|
+
|
|
212
|
+
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
|
|
213
|
+
self._lifespan_result: LifespanResultT | None = None
|
|
214
|
+
self._lifespan_result_set: bool = False
|
|
194
215
|
|
|
195
|
-
if lifespan is None:
|
|
196
|
-
self._has_lifespan = False
|
|
197
|
-
lifespan = default_lifespan
|
|
198
|
-
else:
|
|
199
|
-
self._has_lifespan = True
|
|
200
216
|
# Generate random ID if no name provided
|
|
201
|
-
self._mcp_server = LowLevelServer[
|
|
217
|
+
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
|
|
218
|
+
LifespanResultT
|
|
219
|
+
](
|
|
220
|
+
fastmcp=self,
|
|
202
221
|
name=name or self.generate_name(),
|
|
203
|
-
version=version,
|
|
222
|
+
version=version or fastmcp.__version__,
|
|
204
223
|
instructions=instructions,
|
|
205
|
-
|
|
224
|
+
website_url=website_url,
|
|
225
|
+
icons=icons,
|
|
226
|
+
lifespan=_lifespan_proxy(fastmcp_server=self),
|
|
206
227
|
)
|
|
207
228
|
|
|
208
229
|
# if auth is `NotSet`, try to create a provider from the environment
|
|
209
230
|
if auth is NotSet:
|
|
210
231
|
if fastmcp.settings.server_auth is not None:
|
|
211
|
-
#
|
|
212
|
-
auth = fastmcp.settings.
|
|
232
|
+
# server_auth_class returns the class itself
|
|
233
|
+
auth = fastmcp.settings.server_auth_class()
|
|
213
234
|
else:
|
|
214
235
|
auth = None
|
|
215
|
-
self.auth = cast(AuthProvider | None, auth)
|
|
236
|
+
self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
|
|
216
237
|
|
|
217
238
|
if tools:
|
|
218
239
|
for tool in tools:
|
|
@@ -220,10 +241,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
220
241
|
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
|
221
242
|
self.add_tool(tool)
|
|
222
243
|
|
|
223
|
-
self.include_tags =
|
|
224
|
-
|
|
244
|
+
self.include_tags: set[str] | None = (
|
|
245
|
+
set(include_tags) if include_tags is not None else None
|
|
246
|
+
)
|
|
247
|
+
self.exclude_tags: set[str] | None = (
|
|
248
|
+
set(exclude_tags) if exclude_tags is not None else None
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self.strict_input_validation: bool = (
|
|
252
|
+
strict_input_validation
|
|
253
|
+
if strict_input_validation is not None
|
|
254
|
+
else fastmcp.settings.strict_input_validation
|
|
255
|
+
)
|
|
225
256
|
|
|
226
|
-
self.middleware = middleware or []
|
|
257
|
+
self.middleware: list[Middleware] = list(middleware or [])
|
|
227
258
|
|
|
228
259
|
# Set up MCP protocol handlers
|
|
229
260
|
self._setup_handlers()
|
|
@@ -242,14 +273,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
242
273
|
DeprecationWarning,
|
|
243
274
|
stacklevel=2,
|
|
244
275
|
)
|
|
245
|
-
self.dependencies = (
|
|
276
|
+
self.dependencies: list[str] = (
|
|
246
277
|
dependencies or fastmcp.settings.server_dependencies
|
|
247
278
|
) # TODO: Remove (deprecated in v2.11.4)
|
|
248
279
|
|
|
249
|
-
self.sampling_handler =
|
|
250
|
-
|
|
280
|
+
self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
|
|
281
|
+
sampling_handler
|
|
282
|
+
)
|
|
283
|
+
self.sampling_handler_behavior: Literal["always", "fallback"] = (
|
|
284
|
+
sampling_handler_behavior or "fallback"
|
|
285
|
+
)
|
|
251
286
|
|
|
252
|
-
self.include_fastmcp_meta = (
|
|
287
|
+
self.include_fastmcp_meta: bool = (
|
|
253
288
|
include_fastmcp_meta
|
|
254
289
|
if include_fastmcp_meta is not None
|
|
255
290
|
else fastmcp.settings.include_fastmcp_meta
|
|
@@ -329,10 +364,46 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
329
364
|
def instructions(self) -> str | None:
|
|
330
365
|
return self._mcp_server.instructions
|
|
331
366
|
|
|
367
|
+
@instructions.setter
|
|
368
|
+
def instructions(self, value: str | None) -> None:
|
|
369
|
+
self._mcp_server.instructions = value
|
|
370
|
+
|
|
332
371
|
@property
|
|
333
372
|
def version(self) -> str | None:
|
|
334
373
|
return self._mcp_server.version
|
|
335
374
|
|
|
375
|
+
@property
|
|
376
|
+
def website_url(self) -> str | None:
|
|
377
|
+
return self._mcp_server.website_url
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def icons(self) -> list[mcp.types.Icon]:
|
|
381
|
+
if self._mcp_server.icons is None:
|
|
382
|
+
return []
|
|
383
|
+
else:
|
|
384
|
+
return list(self._mcp_server.icons)
|
|
385
|
+
|
|
386
|
+
@asynccontextmanager
|
|
387
|
+
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
388
|
+
if self._lifespan_result_set:
|
|
389
|
+
yield
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
async with self._lifespan(self) as lifespan_result:
|
|
393
|
+
self._lifespan_result = lifespan_result
|
|
394
|
+
self._lifespan_result_set = True
|
|
395
|
+
|
|
396
|
+
async with AsyncExitStack[bool | None]() as stack:
|
|
397
|
+
for server in self._mounted_servers:
|
|
398
|
+
await stack.enter_async_context(
|
|
399
|
+
cm=server.server._lifespan_manager()
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
yield
|
|
403
|
+
|
|
404
|
+
self._lifespan_result_set = False
|
|
405
|
+
self._lifespan_result = None
|
|
406
|
+
|
|
336
407
|
async def run_async(
|
|
337
408
|
self,
|
|
338
409
|
transport: Transport | None = None,
|
|
@@ -386,13 +457,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
386
457
|
|
|
387
458
|
def _setup_handlers(self) -> None:
|
|
388
459
|
"""Set up core MCP protocol handlers."""
|
|
389
|
-
self._mcp_server.list_tools()(self.
|
|
390
|
-
self._mcp_server.list_resources()(self.
|
|
391
|
-
self._mcp_server.list_resource_templates()(self.
|
|
392
|
-
self._mcp_server.list_prompts()(self.
|
|
393
|
-
self._mcp_server.call_tool(
|
|
394
|
-
|
|
395
|
-
|
|
460
|
+
self._mcp_server.list_tools()(self._list_tools_mcp)
|
|
461
|
+
self._mcp_server.list_resources()(self._list_resources_mcp)
|
|
462
|
+
self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
|
|
463
|
+
self._mcp_server.list_prompts()(self._list_prompts_mcp)
|
|
464
|
+
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
465
|
+
self._call_tool_mcp
|
|
466
|
+
)
|
|
467
|
+
self._mcp_server.read_resource()(self._read_resource_mcp)
|
|
468
|
+
self._mcp_server.get_prompt()(self._get_prompt_mcp)
|
|
396
469
|
|
|
397
470
|
async def _apply_middleware(
|
|
398
471
|
self,
|
|
@@ -409,8 +482,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
409
482
|
self.middleware.append(middleware)
|
|
410
483
|
|
|
411
484
|
async def get_tools(self) -> dict[str, Tool]:
|
|
412
|
-
"""Get all
|
|
413
|
-
|
|
485
|
+
"""Get all tools (unfiltered), including mounted servers, indexed by key."""
|
|
486
|
+
all_tools = dict(await self._tool_manager.get_tools())
|
|
487
|
+
|
|
488
|
+
for mounted in self._mounted_servers:
|
|
489
|
+
try:
|
|
490
|
+
child_tools = await mounted.server.get_tools()
|
|
491
|
+
for key, tool in child_tools.items():
|
|
492
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
493
|
+
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.warning(
|
|
496
|
+
f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
|
|
497
|
+
)
|
|
498
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
499
|
+
raise
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
return all_tools
|
|
414
503
|
|
|
415
504
|
async def get_tool(self, key: str) -> Tool:
|
|
416
505
|
tools = await self.get_tools()
|
|
@@ -419,8 +508,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
419
508
|
return tools[key]
|
|
420
509
|
|
|
421
510
|
async def get_resources(self) -> dict[str, Resource]:
|
|
422
|
-
"""Get all
|
|
423
|
-
|
|
511
|
+
"""Get all resources (unfiltered), including mounted servers, indexed by key."""
|
|
512
|
+
all_resources = dict(await self._resource_manager.get_resources())
|
|
513
|
+
|
|
514
|
+
for mounted in self._mounted_servers:
|
|
515
|
+
try:
|
|
516
|
+
child_resources = await mounted.server.get_resources()
|
|
517
|
+
for key, resource in child_resources.items():
|
|
518
|
+
new_key = (
|
|
519
|
+
add_resource_prefix(
|
|
520
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
521
|
+
)
|
|
522
|
+
if mounted.prefix
|
|
523
|
+
else key
|
|
524
|
+
)
|
|
525
|
+
update = (
|
|
526
|
+
{"name": f"{mounted.prefix}_{resource.name}"}
|
|
527
|
+
if mounted.prefix and resource.name
|
|
528
|
+
else {}
|
|
529
|
+
)
|
|
530
|
+
all_resources[new_key] = resource.model_copy(
|
|
531
|
+
key=new_key, update=update
|
|
532
|
+
)
|
|
533
|
+
except Exception as e:
|
|
534
|
+
logger.warning(
|
|
535
|
+
f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
|
|
536
|
+
)
|
|
537
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
538
|
+
raise
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
return all_resources
|
|
424
542
|
|
|
425
543
|
async def get_resource(self, key: str) -> Resource:
|
|
426
544
|
resources = await self.get_resources()
|
|
@@ -429,8 +547,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
429
547
|
return resources[key]
|
|
430
548
|
|
|
431
549
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
432
|
-
"""Get all
|
|
433
|
-
|
|
550
|
+
"""Get all resource templates (unfiltered), including mounted servers, indexed by key."""
|
|
551
|
+
all_templates = dict(await self._resource_manager.get_resource_templates())
|
|
552
|
+
|
|
553
|
+
for mounted in self._mounted_servers:
|
|
554
|
+
try:
|
|
555
|
+
child_templates = await mounted.server.get_resource_templates()
|
|
556
|
+
for key, template in child_templates.items():
|
|
557
|
+
new_key = (
|
|
558
|
+
add_resource_prefix(
|
|
559
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
560
|
+
)
|
|
561
|
+
if mounted.prefix
|
|
562
|
+
else key
|
|
563
|
+
)
|
|
564
|
+
update = (
|
|
565
|
+
{"name": f"{mounted.prefix}_{template.name}"}
|
|
566
|
+
if mounted.prefix and template.name
|
|
567
|
+
else {}
|
|
568
|
+
)
|
|
569
|
+
all_templates[new_key] = template.model_copy(
|
|
570
|
+
key=new_key, update=update
|
|
571
|
+
)
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.warning(
|
|
574
|
+
f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
|
|
575
|
+
)
|
|
576
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
577
|
+
raise
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
return all_templates
|
|
434
581
|
|
|
435
582
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
436
583
|
"""Get a registered resource template by key."""
|
|
@@ -440,10 +587,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
440
587
|
return templates[key]
|
|
441
588
|
|
|
442
589
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
443
|
-
"""
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
590
|
+
"""Get all prompts (unfiltered), including mounted servers, indexed by key."""
|
|
591
|
+
all_prompts = dict(await self._prompt_manager.get_prompts())
|
|
592
|
+
|
|
593
|
+
for mounted in self._mounted_servers:
|
|
594
|
+
try:
|
|
595
|
+
child_prompts = await mounted.server.get_prompts()
|
|
596
|
+
for key, prompt in child_prompts.items():
|
|
597
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
598
|
+
all_prompts[new_key] = prompt.model_copy(key=new_key)
|
|
599
|
+
except Exception as e:
|
|
600
|
+
logger.warning(
|
|
601
|
+
f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
|
|
602
|
+
)
|
|
603
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
604
|
+
raise
|
|
605
|
+
continue
|
|
606
|
+
|
|
607
|
+
return all_prompts
|
|
447
608
|
|
|
448
609
|
async def get_prompt(self, key: str) -> Prompt:
|
|
449
610
|
prompts = await self.get_prompts()
|
|
@@ -519,11 +680,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
519
680
|
|
|
520
681
|
return routes
|
|
521
682
|
|
|
522
|
-
async def
|
|
683
|
+
async def _list_tools_mcp(self) -> list[MCPTool]:
|
|
684
|
+
"""
|
|
685
|
+
List all available tools, in the format expected by the low-level MCP
|
|
686
|
+
server.
|
|
687
|
+
"""
|
|
523
688
|
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
524
689
|
|
|
525
690
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
526
|
-
tools = await self.
|
|
691
|
+
tools = await self._list_tools_middleware()
|
|
527
692
|
return [
|
|
528
693
|
tool.to_mcp_tool(
|
|
529
694
|
name=tool.key,
|
|
@@ -532,24 +697,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
532
697
|
for tool in tools
|
|
533
698
|
]
|
|
534
699
|
|
|
535
|
-
async def
|
|
700
|
+
async def _list_tools_middleware(self) -> list[Tool]:
|
|
536
701
|
"""
|
|
537
|
-
List all available tools,
|
|
538
|
-
server.
|
|
702
|
+
List all available tools, applying MCP middleware.
|
|
539
703
|
"""
|
|
540
704
|
|
|
541
|
-
async def _handler(
|
|
542
|
-
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
543
|
-
) -> list[Tool]:
|
|
544
|
-
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
|
545
|
-
|
|
546
|
-
mcp_tools: list[Tool] = []
|
|
547
|
-
for tool in tools:
|
|
548
|
-
if self._should_enable_component(tool):
|
|
549
|
-
mcp_tools.append(tool)
|
|
550
|
-
|
|
551
|
-
return mcp_tools
|
|
552
|
-
|
|
553
705
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
554
706
|
# Create the middleware context.
|
|
555
707
|
mw_context = MiddlewareContext(
|
|
@@ -561,13 +713,66 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
561
713
|
)
|
|
562
714
|
|
|
563
715
|
# Apply the middleware chain.
|
|
564
|
-
return
|
|
716
|
+
return list(
|
|
717
|
+
await self._apply_middleware(
|
|
718
|
+
context=mw_context, call_next=self._list_tools
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
async def _list_tools(
|
|
723
|
+
self,
|
|
724
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
725
|
+
) -> list[Tool]:
|
|
726
|
+
"""
|
|
727
|
+
List all available tools.
|
|
728
|
+
"""
|
|
729
|
+
# 1. Get local tools and filter them
|
|
730
|
+
local_tools = await self._tool_manager.get_tools()
|
|
731
|
+
filtered_local = [
|
|
732
|
+
tool for tool in local_tools.values() if self._should_enable_component(tool)
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
# 2. Get tools from mounted servers
|
|
736
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
737
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
738
|
+
all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
|
|
739
|
+
|
|
740
|
+
for mounted in self._mounted_servers:
|
|
741
|
+
try:
|
|
742
|
+
child_tools = await mounted.server._list_tools_middleware()
|
|
743
|
+
for tool in child_tools:
|
|
744
|
+
# Apply parent server's filtering to mounted components
|
|
745
|
+
if not self._should_enable_component(tool):
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
key = tool.key
|
|
749
|
+
if mounted.prefix:
|
|
750
|
+
key = f"{mounted.prefix}_{tool.key}"
|
|
751
|
+
tool = tool.model_copy(key=key)
|
|
752
|
+
# Later mounted servers override earlier ones
|
|
753
|
+
all_tools[key] = tool
|
|
754
|
+
except Exception as e:
|
|
755
|
+
server_name = getattr(
|
|
756
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
757
|
+
)
|
|
758
|
+
logger.warning(
|
|
759
|
+
f"Failed to list tools from mounted server {server_name!r}: {e}"
|
|
760
|
+
)
|
|
761
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
762
|
+
raise
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
return list(all_tools.values())
|
|
565
766
|
|
|
566
|
-
async def
|
|
767
|
+
async def _list_resources_mcp(self) -> list[MCPResource]:
|
|
768
|
+
"""
|
|
769
|
+
List all available resources, in the format expected by the low-level MCP
|
|
770
|
+
server.
|
|
771
|
+
"""
|
|
567
772
|
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
568
773
|
|
|
569
774
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
570
|
-
resources = await self.
|
|
775
|
+
resources = await self._list_resources_middleware()
|
|
571
776
|
return [
|
|
572
777
|
resource.to_mcp_resource(
|
|
573
778
|
uri=resource.key,
|
|
@@ -576,25 +781,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
576
781
|
for resource in resources
|
|
577
782
|
]
|
|
578
783
|
|
|
579
|
-
async def
|
|
784
|
+
async def _list_resources_middleware(self) -> list[Resource]:
|
|
580
785
|
"""
|
|
581
|
-
List all available resources,
|
|
582
|
-
server.
|
|
583
|
-
|
|
786
|
+
List all available resources, applying MCP middleware.
|
|
584
787
|
"""
|
|
585
788
|
|
|
586
|
-
async def _handler(
|
|
587
|
-
context: MiddlewareContext[dict[str, Any]],
|
|
588
|
-
) -> list[Resource]:
|
|
589
|
-
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
|
590
|
-
|
|
591
|
-
mcp_resources: list[Resource] = []
|
|
592
|
-
for resource in resources:
|
|
593
|
-
if self._should_enable_component(resource):
|
|
594
|
-
mcp_resources.append(resource)
|
|
595
|
-
|
|
596
|
-
return mcp_resources
|
|
597
|
-
|
|
598
789
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
599
790
|
# Create the middleware context.
|
|
600
791
|
mw_context = MiddlewareContext(
|
|
@@ -606,13 +797,75 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
606
797
|
)
|
|
607
798
|
|
|
608
799
|
# Apply the middleware chain.
|
|
609
|
-
return
|
|
800
|
+
return list(
|
|
801
|
+
await self._apply_middleware(
|
|
802
|
+
context=mw_context, call_next=self._list_resources
|
|
803
|
+
)
|
|
804
|
+
)
|
|
610
805
|
|
|
611
|
-
async def
|
|
806
|
+
async def _list_resources(
|
|
807
|
+
self,
|
|
808
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
809
|
+
) -> list[Resource]:
|
|
810
|
+
"""
|
|
811
|
+
List all available resources.
|
|
812
|
+
"""
|
|
813
|
+
# 1. Filter local resources
|
|
814
|
+
local_resources = await self._resource_manager.get_resources()
|
|
815
|
+
filtered_local = [
|
|
816
|
+
resource
|
|
817
|
+
for resource in local_resources.values()
|
|
818
|
+
if self._should_enable_component(resource)
|
|
819
|
+
]
|
|
820
|
+
|
|
821
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
822
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
823
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
824
|
+
all_resources: dict[str, Resource] = {
|
|
825
|
+
resource.key: resource for resource in filtered_local
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
for mounted in self._mounted_servers:
|
|
829
|
+
try:
|
|
830
|
+
child_resources = await mounted.server._list_resources_middleware()
|
|
831
|
+
for resource in child_resources:
|
|
832
|
+
# Apply parent server's filtering to mounted components
|
|
833
|
+
if not self._should_enable_component(resource):
|
|
834
|
+
continue
|
|
835
|
+
|
|
836
|
+
key = resource.key
|
|
837
|
+
if mounted.prefix:
|
|
838
|
+
key = add_resource_prefix(
|
|
839
|
+
resource.key,
|
|
840
|
+
mounted.prefix,
|
|
841
|
+
mounted.resource_prefix_format,
|
|
842
|
+
)
|
|
843
|
+
resource = resource.model_copy(
|
|
844
|
+
key=key,
|
|
845
|
+
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
846
|
+
)
|
|
847
|
+
# Later mounted servers override earlier ones
|
|
848
|
+
all_resources[key] = resource
|
|
849
|
+
except Exception as e:
|
|
850
|
+
server_name = getattr(
|
|
851
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
852
|
+
)
|
|
853
|
+
logger.warning(f"Failed to list resources from {server_name!r}: {e}")
|
|
854
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
855
|
+
raise
|
|
856
|
+
continue
|
|
857
|
+
|
|
858
|
+
return list(all_resources.values())
|
|
859
|
+
|
|
860
|
+
async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
|
|
861
|
+
"""
|
|
862
|
+
List all available resource templates, in the format expected by the low-level MCP
|
|
863
|
+
server.
|
|
864
|
+
"""
|
|
612
865
|
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
613
866
|
|
|
614
867
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
615
|
-
templates = await self.
|
|
868
|
+
templates = await self._list_resource_templates_middleware()
|
|
616
869
|
return [
|
|
617
870
|
template.to_mcp_template(
|
|
618
871
|
uriTemplate=template.key,
|
|
@@ -621,25 +874,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
621
874
|
for template in templates
|
|
622
875
|
]
|
|
623
876
|
|
|
624
|
-
async def
|
|
877
|
+
async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
|
|
625
878
|
"""
|
|
626
|
-
List all available resource templates,
|
|
627
|
-
server.
|
|
879
|
+
List all available resource templates, applying MCP middleware.
|
|
628
880
|
|
|
629
881
|
"""
|
|
630
882
|
|
|
631
|
-
async def _handler(
|
|
632
|
-
context: MiddlewareContext[dict[str, Any]],
|
|
633
|
-
) -> list[ResourceTemplate]:
|
|
634
|
-
templates = await self._resource_manager.list_resource_templates()
|
|
635
|
-
|
|
636
|
-
mcp_templates: list[ResourceTemplate] = []
|
|
637
|
-
for template in templates:
|
|
638
|
-
if self._should_enable_component(template):
|
|
639
|
-
mcp_templates.append(template)
|
|
640
|
-
|
|
641
|
-
return mcp_templates
|
|
642
|
-
|
|
643
883
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
644
884
|
# Create the middleware context.
|
|
645
885
|
mw_context = MiddlewareContext(
|
|
@@ -651,13 +891,79 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
651
891
|
)
|
|
652
892
|
|
|
653
893
|
# Apply the middleware chain.
|
|
654
|
-
return
|
|
894
|
+
return list(
|
|
895
|
+
await self._apply_middleware(
|
|
896
|
+
context=mw_context, call_next=self._list_resource_templates
|
|
897
|
+
)
|
|
898
|
+
)
|
|
655
899
|
|
|
656
|
-
async def
|
|
900
|
+
async def _list_resource_templates(
|
|
901
|
+
self,
|
|
902
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
903
|
+
) -> list[ResourceTemplate]:
|
|
904
|
+
"""
|
|
905
|
+
List all available resource templates.
|
|
906
|
+
"""
|
|
907
|
+
# 1. Filter local templates
|
|
908
|
+
local_templates = await self._resource_manager.get_resource_templates()
|
|
909
|
+
filtered_local = [
|
|
910
|
+
template
|
|
911
|
+
for template in local_templates.values()
|
|
912
|
+
if self._should_enable_component(template)
|
|
913
|
+
]
|
|
914
|
+
|
|
915
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
916
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
917
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
918
|
+
all_templates: dict[str, ResourceTemplate] = {
|
|
919
|
+
template.key: template for template in filtered_local
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
for mounted in self._mounted_servers:
|
|
923
|
+
try:
|
|
924
|
+
child_templates = (
|
|
925
|
+
await mounted.server._list_resource_templates_middleware()
|
|
926
|
+
)
|
|
927
|
+
for template in child_templates:
|
|
928
|
+
# Apply parent server's filtering to mounted components
|
|
929
|
+
if not self._should_enable_component(template):
|
|
930
|
+
continue
|
|
931
|
+
|
|
932
|
+
key = template.key
|
|
933
|
+
if mounted.prefix:
|
|
934
|
+
key = add_resource_prefix(
|
|
935
|
+
template.key,
|
|
936
|
+
mounted.prefix,
|
|
937
|
+
mounted.resource_prefix_format,
|
|
938
|
+
)
|
|
939
|
+
template = template.model_copy(
|
|
940
|
+
key=key,
|
|
941
|
+
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
942
|
+
)
|
|
943
|
+
# Later mounted servers override earlier ones
|
|
944
|
+
all_templates[key] = template
|
|
945
|
+
except Exception as e:
|
|
946
|
+
server_name = getattr(
|
|
947
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
948
|
+
)
|
|
949
|
+
logger.warning(
|
|
950
|
+
f"Failed to list resource templates from {server_name!r}: {e}"
|
|
951
|
+
)
|
|
952
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
953
|
+
raise
|
|
954
|
+
continue
|
|
955
|
+
|
|
956
|
+
return list(all_templates.values())
|
|
957
|
+
|
|
958
|
+
async def _list_prompts_mcp(self) -> list[MCPPrompt]:
|
|
959
|
+
"""
|
|
960
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
961
|
+
server.
|
|
962
|
+
"""
|
|
657
963
|
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
|
658
964
|
|
|
659
965
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
660
|
-
prompts = await self.
|
|
966
|
+
prompts = await self._list_prompts_middleware()
|
|
661
967
|
return [
|
|
662
968
|
prompt.to_mcp_prompt(
|
|
663
969
|
name=prompt.key,
|
|
@@ -666,25 +972,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
666
972
|
for prompt in prompts
|
|
667
973
|
]
|
|
668
974
|
|
|
669
|
-
async def
|
|
975
|
+
async def _list_prompts_middleware(self) -> list[Prompt]:
|
|
670
976
|
"""
|
|
671
|
-
List all available prompts,
|
|
672
|
-
server.
|
|
977
|
+
List all available prompts, applying MCP middleware.
|
|
673
978
|
|
|
674
979
|
"""
|
|
675
980
|
|
|
676
|
-
async def _handler(
|
|
677
|
-
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
678
|
-
) -> list[Prompt]:
|
|
679
|
-
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
|
680
|
-
|
|
681
|
-
mcp_prompts: list[Prompt] = []
|
|
682
|
-
for prompt in prompts:
|
|
683
|
-
if self._should_enable_component(prompt):
|
|
684
|
-
mcp_prompts.append(prompt)
|
|
685
|
-
|
|
686
|
-
return mcp_prompts
|
|
687
|
-
|
|
688
981
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
689
982
|
# Create the middleware context.
|
|
690
983
|
mw_context = MiddlewareContext(
|
|
@@ -696,11 +989,68 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
696
989
|
)
|
|
697
990
|
|
|
698
991
|
# Apply the middleware chain.
|
|
699
|
-
return
|
|
992
|
+
return list(
|
|
993
|
+
await self._apply_middleware(
|
|
994
|
+
context=mw_context, call_next=self._list_prompts
|
|
995
|
+
)
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
async def _list_prompts(
|
|
999
|
+
self,
|
|
1000
|
+
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
1001
|
+
) -> list[Prompt]:
|
|
1002
|
+
"""
|
|
1003
|
+
List all available prompts.
|
|
1004
|
+
"""
|
|
1005
|
+
# 1. Filter local prompts
|
|
1006
|
+
local_prompts = await self._prompt_manager.get_prompts()
|
|
1007
|
+
filtered_local = [
|
|
1008
|
+
prompt
|
|
1009
|
+
for prompt in local_prompts.values()
|
|
1010
|
+
if self._should_enable_component(prompt)
|
|
1011
|
+
]
|
|
1012
|
+
|
|
1013
|
+
# 2. Get from mounted servers
|
|
1014
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1015
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
1016
|
+
all_prompts: dict[str, Prompt] = {
|
|
1017
|
+
prompt.key: prompt for prompt in filtered_local
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
for mounted in self._mounted_servers:
|
|
1021
|
+
try:
|
|
1022
|
+
child_prompts = await mounted.server._list_prompts_middleware()
|
|
1023
|
+
for prompt in child_prompts:
|
|
1024
|
+
# Apply parent server's filtering to mounted components
|
|
1025
|
+
if not self._should_enable_component(prompt):
|
|
1026
|
+
continue
|
|
1027
|
+
|
|
1028
|
+
key = prompt.key
|
|
1029
|
+
if mounted.prefix:
|
|
1030
|
+
key = f"{mounted.prefix}_{prompt.key}"
|
|
1031
|
+
prompt = prompt.model_copy(key=key)
|
|
1032
|
+
# Later mounted servers override earlier ones
|
|
1033
|
+
all_prompts[key] = prompt
|
|
1034
|
+
except Exception as e:
|
|
1035
|
+
server_name = getattr(
|
|
1036
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1037
|
+
)
|
|
1038
|
+
logger.warning(
|
|
1039
|
+
f"Failed to list prompts from mounted server {server_name!r}: {e}"
|
|
1040
|
+
)
|
|
1041
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1042
|
+
raise
|
|
1043
|
+
continue
|
|
700
1044
|
|
|
701
|
-
|
|
1045
|
+
return list(all_prompts.values())
|
|
1046
|
+
|
|
1047
|
+
async def _call_tool_mcp(
|
|
702
1048
|
self, key: str, arguments: dict[str, Any]
|
|
703
|
-
) ->
|
|
1049
|
+
) -> (
|
|
1050
|
+
list[ContentBlock]
|
|
1051
|
+
| tuple[list[ContentBlock], dict[str, Any]]
|
|
1052
|
+
| mcp.types.CallToolResult
|
|
1053
|
+
):
|
|
704
1054
|
"""
|
|
705
1055
|
Handle MCP 'callTool' requests.
|
|
706
1056
|
|
|
@@ -719,29 +1069,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
719
1069
|
|
|
720
1070
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
721
1071
|
try:
|
|
722
|
-
result = await self.
|
|
1072
|
+
result = await self._call_tool_middleware(key, arguments)
|
|
723
1073
|
return result.to_mcp_result()
|
|
724
|
-
except DisabledError:
|
|
725
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
726
|
-
except NotFoundError:
|
|
727
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
1074
|
+
except DisabledError as e:
|
|
1075
|
+
raise NotFoundError(f"Unknown tool: {key}") from e
|
|
1076
|
+
except NotFoundError as e:
|
|
1077
|
+
raise NotFoundError(f"Unknown tool: {key}") from e
|
|
728
1078
|
|
|
729
|
-
async def
|
|
1079
|
+
async def _call_tool_middleware(
|
|
1080
|
+
self,
|
|
1081
|
+
key: str,
|
|
1082
|
+
arguments: dict[str, Any],
|
|
1083
|
+
) -> ToolResult:
|
|
730
1084
|
"""
|
|
731
1085
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
732
1086
|
"""
|
|
733
1087
|
|
|
734
|
-
async def _handler(
|
|
735
|
-
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
736
|
-
) -> ToolResult:
|
|
737
|
-
tool = await self._tool_manager.get_tool(context.message.name)
|
|
738
|
-
if not self._should_enable_component(tool):
|
|
739
|
-
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
|
740
|
-
|
|
741
|
-
return await self._tool_manager.call_tool(
|
|
742
|
-
key=context.message.name, arguments=context.message.arguments or {}
|
|
743
|
-
)
|
|
744
|
-
|
|
745
1088
|
mw_context = MiddlewareContext[CallToolRequestParams](
|
|
746
1089
|
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
|
747
1090
|
source="client",
|
|
@@ -749,9 +1092,53 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
749
1092
|
method="tools/call",
|
|
750
1093
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
751
1094
|
)
|
|
752
|
-
return await self._apply_middleware(
|
|
1095
|
+
return await self._apply_middleware(
|
|
1096
|
+
context=mw_context, call_next=self._call_tool
|
|
1097
|
+
)
|
|
753
1098
|
|
|
754
|
-
async def
|
|
1099
|
+
async def _call_tool(
|
|
1100
|
+
self,
|
|
1101
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
1102
|
+
) -> ToolResult:
|
|
1103
|
+
"""
|
|
1104
|
+
Call a tool
|
|
1105
|
+
"""
|
|
1106
|
+
tool_name = context.message.name
|
|
1107
|
+
|
|
1108
|
+
# Try mounted servers in reverse order (later wins)
|
|
1109
|
+
for mounted in reversed(self._mounted_servers):
|
|
1110
|
+
try_name = tool_name
|
|
1111
|
+
if mounted.prefix:
|
|
1112
|
+
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1113
|
+
continue
|
|
1114
|
+
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
# First, get the tool to check if parent's filter allows it
|
|
1118
|
+
tool = await mounted.server._tool_manager.get_tool(try_name)
|
|
1119
|
+
if not self._should_enable_component(tool):
|
|
1120
|
+
# Parent filter blocks this tool, continue searching
|
|
1121
|
+
continue
|
|
1122
|
+
|
|
1123
|
+
return await mounted.server._call_tool_middleware(
|
|
1124
|
+
try_name, context.message.arguments or {}
|
|
1125
|
+
)
|
|
1126
|
+
except NotFoundError:
|
|
1127
|
+
continue
|
|
1128
|
+
|
|
1129
|
+
# Try local tools last (mounted servers override local)
|
|
1130
|
+
try:
|
|
1131
|
+
tool = await self._tool_manager.get_tool(tool_name)
|
|
1132
|
+
if self._should_enable_component(tool):
|
|
1133
|
+
return await self._tool_manager.call_tool(
|
|
1134
|
+
key=tool_name, arguments=context.message.arguments or {}
|
|
1135
|
+
)
|
|
1136
|
+
except NotFoundError:
|
|
1137
|
+
pass
|
|
1138
|
+
|
|
1139
|
+
raise NotFoundError(f"Unknown tool: {tool_name!r}")
|
|
1140
|
+
|
|
1141
|
+
async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
755
1142
|
"""
|
|
756
1143
|
Handle MCP 'readResource' requests.
|
|
757
1144
|
|
|
@@ -761,39 +1148,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
761
1148
|
|
|
762
1149
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
763
1150
|
try:
|
|
764
|
-
return
|
|
765
|
-
|
|
1151
|
+
return list[ReadResourceContents](
|
|
1152
|
+
await self._read_resource_middleware(uri)
|
|
1153
|
+
)
|
|
1154
|
+
except DisabledError as e:
|
|
766
1155
|
# convert to NotFoundError to avoid leaking resource presence
|
|
767
|
-
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
768
|
-
except NotFoundError:
|
|
1156
|
+
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
|
|
1157
|
+
except NotFoundError as e:
|
|
769
1158
|
# standardize NotFound message
|
|
770
|
-
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
1159
|
+
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
|
|
771
1160
|
|
|
772
|
-
async def
|
|
1161
|
+
async def _read_resource_middleware(
|
|
1162
|
+
self,
|
|
1163
|
+
uri: AnyUrl | str,
|
|
1164
|
+
) -> list[ReadResourceContents]:
|
|
773
1165
|
"""
|
|
774
1166
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
775
1167
|
"""
|
|
776
1168
|
|
|
777
|
-
async def _handler(
|
|
778
|
-
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
779
|
-
) -> list[ReadResourceContents]:
|
|
780
|
-
resource = await self._resource_manager.get_resource(context.message.uri)
|
|
781
|
-
if not self._should_enable_component(resource):
|
|
782
|
-
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
|
|
783
|
-
|
|
784
|
-
content = await self._resource_manager.read_resource(context.message.uri)
|
|
785
|
-
return [
|
|
786
|
-
ReadResourceContents(
|
|
787
|
-
content=content,
|
|
788
|
-
mime_type=resource.mime_type,
|
|
789
|
-
)
|
|
790
|
-
]
|
|
791
|
-
|
|
792
1169
|
# Convert string URI to AnyUrl if needed
|
|
793
|
-
if isinstance(uri, str)
|
|
794
|
-
uri_param = AnyUrl(uri)
|
|
795
|
-
else:
|
|
796
|
-
uri_param = uri
|
|
1170
|
+
uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
|
|
797
1171
|
|
|
798
1172
|
mw_context = MiddlewareContext(
|
|
799
1173
|
message=mcp.types.ReadResourceRequestParams(uri=uri_param),
|
|
@@ -802,9 +1176,61 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
802
1176
|
method="resources/read",
|
|
803
1177
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
804
1178
|
)
|
|
805
|
-
return
|
|
1179
|
+
return list(
|
|
1180
|
+
await self._apply_middleware(
|
|
1181
|
+
context=mw_context, call_next=self._read_resource
|
|
1182
|
+
)
|
|
1183
|
+
)
|
|
806
1184
|
|
|
807
|
-
async def
|
|
1185
|
+
async def _read_resource(
|
|
1186
|
+
self,
|
|
1187
|
+
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
1188
|
+
) -> list[ReadResourceContents]:
|
|
1189
|
+
"""
|
|
1190
|
+
Read a resource
|
|
1191
|
+
"""
|
|
1192
|
+
uri_str = str(context.message.uri)
|
|
1193
|
+
|
|
1194
|
+
# Try mounted servers in reverse order (later wins)
|
|
1195
|
+
for mounted in reversed(self._mounted_servers):
|
|
1196
|
+
key = uri_str
|
|
1197
|
+
if mounted.prefix:
|
|
1198
|
+
if not has_resource_prefix(
|
|
1199
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
1200
|
+
):
|
|
1201
|
+
continue
|
|
1202
|
+
key = remove_resource_prefix(
|
|
1203
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
try:
|
|
1207
|
+
# First, get the resource to check if parent's filter allows it
|
|
1208
|
+
resource = await mounted.server._resource_manager.get_resource(key)
|
|
1209
|
+
if not self._should_enable_component(resource):
|
|
1210
|
+
# Parent filter blocks this resource, continue searching
|
|
1211
|
+
continue
|
|
1212
|
+
result = list(await mounted.server._read_resource_middleware(key))
|
|
1213
|
+
return result
|
|
1214
|
+
except NotFoundError:
|
|
1215
|
+
continue
|
|
1216
|
+
|
|
1217
|
+
# Try local resources last (mounted servers override local)
|
|
1218
|
+
try:
|
|
1219
|
+
resource = await self._resource_manager.get_resource(uri_str)
|
|
1220
|
+
if self._should_enable_component(resource):
|
|
1221
|
+
content = await self._resource_manager.read_resource(uri_str)
|
|
1222
|
+
return [
|
|
1223
|
+
ReadResourceContents(
|
|
1224
|
+
content=content,
|
|
1225
|
+
mime_type=resource.mime_type,
|
|
1226
|
+
)
|
|
1227
|
+
]
|
|
1228
|
+
except NotFoundError:
|
|
1229
|
+
pass
|
|
1230
|
+
|
|
1231
|
+
raise NotFoundError(f"Unknown resource: {uri_str!r}")
|
|
1232
|
+
|
|
1233
|
+
async def _get_prompt_mcp(
|
|
808
1234
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
809
1235
|
) -> GetPromptResult:
|
|
810
1236
|
"""
|
|
@@ -812,38 +1238,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
812
1238
|
|
|
813
1239
|
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
|
|
814
1240
|
"""
|
|
1241
|
+
import fastmcp.server.context
|
|
1242
|
+
|
|
815
1243
|
logger.debug(
|
|
816
1244
|
f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
|
|
817
1245
|
)
|
|
818
1246
|
|
|
819
1247
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
820
1248
|
try:
|
|
821
|
-
return await self.
|
|
822
|
-
except DisabledError:
|
|
1249
|
+
return await self._get_prompt_middleware(name, arguments)
|
|
1250
|
+
except DisabledError as e:
|
|
823
1251
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
824
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
825
|
-
except NotFoundError:
|
|
1252
|
+
raise NotFoundError(f"Unknown prompt: {name}") from e
|
|
1253
|
+
except NotFoundError as e:
|
|
826
1254
|
# standardize NotFound message
|
|
827
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
1255
|
+
raise NotFoundError(f"Unknown prompt: {name}") from e
|
|
828
1256
|
|
|
829
|
-
async def
|
|
1257
|
+
async def _get_prompt_middleware(
|
|
830
1258
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
831
1259
|
) -> GetPromptResult:
|
|
832
1260
|
"""
|
|
833
1261
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
834
1262
|
"""
|
|
835
1263
|
|
|
836
|
-
async def _handler(
|
|
837
|
-
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
838
|
-
) -> GetPromptResult:
|
|
839
|
-
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
|
840
|
-
if not self._should_enable_component(prompt):
|
|
841
|
-
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
|
842
|
-
|
|
843
|
-
return await self._prompt_manager.render_prompt(
|
|
844
|
-
name=context.message.name, arguments=context.message.arguments
|
|
845
|
-
)
|
|
846
|
-
|
|
847
1264
|
mw_context = MiddlewareContext(
|
|
848
1265
|
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
849
1266
|
source="client",
|
|
@@ -851,7 +1268,47 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
851
1268
|
method="prompts/get",
|
|
852
1269
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
853
1270
|
)
|
|
854
|
-
return await self._apply_middleware(
|
|
1271
|
+
return await self._apply_middleware(
|
|
1272
|
+
context=mw_context, call_next=self._get_prompt
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
async def _get_prompt(
|
|
1276
|
+
self,
|
|
1277
|
+
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
1278
|
+
) -> GetPromptResult:
|
|
1279
|
+
name = context.message.name
|
|
1280
|
+
|
|
1281
|
+
# Try mounted servers in reverse order (later wins)
|
|
1282
|
+
for mounted in reversed(self._mounted_servers):
|
|
1283
|
+
try_name = name
|
|
1284
|
+
if mounted.prefix:
|
|
1285
|
+
if not name.startswith(f"{mounted.prefix}_"):
|
|
1286
|
+
continue
|
|
1287
|
+
try_name = name[len(mounted.prefix) + 1 :]
|
|
1288
|
+
|
|
1289
|
+
try:
|
|
1290
|
+
# First, get the prompt to check if parent's filter allows it
|
|
1291
|
+
prompt = await mounted.server._prompt_manager.get_prompt(try_name)
|
|
1292
|
+
if not self._should_enable_component(prompt):
|
|
1293
|
+
# Parent filter blocks this prompt, continue searching
|
|
1294
|
+
continue
|
|
1295
|
+
return await mounted.server._get_prompt_middleware(
|
|
1296
|
+
try_name, context.message.arguments
|
|
1297
|
+
)
|
|
1298
|
+
except NotFoundError:
|
|
1299
|
+
continue
|
|
1300
|
+
|
|
1301
|
+
# Try local prompts last (mounted servers override local)
|
|
1302
|
+
try:
|
|
1303
|
+
prompt = await self._prompt_manager.get_prompt(name)
|
|
1304
|
+
if self._should_enable_component(prompt):
|
|
1305
|
+
return await self._prompt_manager.render_prompt(
|
|
1306
|
+
name=name, arguments=context.message.arguments
|
|
1307
|
+
)
|
|
1308
|
+
except NotFoundError:
|
|
1309
|
+
pass
|
|
1310
|
+
|
|
1311
|
+
raise NotFoundError(f"Unknown prompt: {name!r}")
|
|
855
1312
|
|
|
856
1313
|
def add_tool(self, tool: Tool) -> Tool:
|
|
857
1314
|
"""Add a tool to the server.
|
|
@@ -916,8 +1373,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
916
1373
|
name: str | None = None,
|
|
917
1374
|
title: str | None = None,
|
|
918
1375
|
description: str | None = None,
|
|
1376
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
919
1377
|
tags: set[str] | None = None,
|
|
920
|
-
output_schema: dict[str, Any] |
|
|
1378
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
921
1379
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
922
1380
|
exclude_args: list[str] | None = None,
|
|
923
1381
|
meta: dict[str, Any] | None = None,
|
|
@@ -932,8 +1390,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
932
1390
|
name: str | None = None,
|
|
933
1391
|
title: str | None = None,
|
|
934
1392
|
description: str | None = None,
|
|
1393
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
935
1394
|
tags: set[str] | None = None,
|
|
936
|
-
output_schema: dict[str, Any] |
|
|
1395
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
937
1396
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
938
1397
|
exclude_args: list[str] | None = None,
|
|
939
1398
|
meta: dict[str, Any] | None = None,
|
|
@@ -947,8 +1406,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
947
1406
|
name: str | None = None,
|
|
948
1407
|
title: str | None = None,
|
|
949
1408
|
description: str | None = None,
|
|
1409
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
950
1410
|
tags: set[str] | None = None,
|
|
951
|
-
output_schema: dict[str, Any] |
|
|
1411
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
952
1412
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
953
1413
|
exclude_args: list[str] | None = None,
|
|
954
1414
|
meta: dict[str, Any] | None = None,
|
|
@@ -974,7 +1434,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
974
1434
|
tags: Optional set of tags for categorizing the tool
|
|
975
1435
|
output_schema: Optional JSON schema for the tool's output
|
|
976
1436
|
annotations: Optional annotations about the tool's behavior
|
|
977
|
-
exclude_args: Optional list of argument names to exclude from the tool schema
|
|
1437
|
+
exclude_args: Optional list of argument names to exclude from the tool schema.
|
|
1438
|
+
Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
|
|
1439
|
+
injection with `Depends()` for better lifecycle management.
|
|
978
1440
|
meta: Optional meta information about the tool
|
|
979
1441
|
enabled: Optional boolean to enable or disable the tool
|
|
980
1442
|
|
|
@@ -1025,14 +1487,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1025
1487
|
tool_name = name # Use keyword name if provided, otherwise None
|
|
1026
1488
|
|
|
1027
1489
|
# Register the tool immediately and return the tool object
|
|
1490
|
+
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
|
1028
1491
|
tool = Tool.from_function(
|
|
1029
1492
|
fn,
|
|
1030
1493
|
name=tool_name,
|
|
1031
1494
|
title=title,
|
|
1032
1495
|
description=description,
|
|
1496
|
+
icons=icons,
|
|
1033
1497
|
tags=tags,
|
|
1034
1498
|
output_schema=output_schema,
|
|
1035
|
-
annotations=
|
|
1499
|
+
annotations=annotations,
|
|
1036
1500
|
exclude_args=exclude_args,
|
|
1037
1501
|
meta=meta,
|
|
1038
1502
|
serializer=self._tool_serializer,
|
|
@@ -1063,6 +1527,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1063
1527
|
name=tool_name,
|
|
1064
1528
|
title=title,
|
|
1065
1529
|
description=description,
|
|
1530
|
+
icons=icons,
|
|
1066
1531
|
tags=tags,
|
|
1067
1532
|
output_schema=output_schema,
|
|
1068
1533
|
annotations=annotations,
|
|
@@ -1160,6 +1625,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1160
1625
|
name: str | None = None,
|
|
1161
1626
|
title: str | None = None,
|
|
1162
1627
|
description: str | None = None,
|
|
1628
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1163
1629
|
mime_type: str | None = None,
|
|
1164
1630
|
tags: set[str] | None = None,
|
|
1165
1631
|
enabled: bool | None = None,
|
|
@@ -1208,8 +1674,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1208
1674
|
return f"Weather for {city}"
|
|
1209
1675
|
|
|
1210
1676
|
@server.resource("resource://{city}/weather")
|
|
1211
|
-
def get_weather_with_context(city: str, ctx: Context) -> str:
|
|
1212
|
-
ctx.info(f"Fetching weather for {city}")
|
|
1677
|
+
async def get_weather_with_context(city: str, ctx: Context) -> str:
|
|
1678
|
+
await ctx.info(f"Fetching weather for {city}")
|
|
1213
1679
|
return f"Weather for {city}"
|
|
1214
1680
|
|
|
1215
1681
|
@server.resource("resource://{city}/weather")
|
|
@@ -1259,10 +1725,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1259
1725
|
name=name,
|
|
1260
1726
|
title=title,
|
|
1261
1727
|
description=description,
|
|
1728
|
+
icons=icons,
|
|
1262
1729
|
mime_type=mime_type,
|
|
1263
1730
|
tags=tags,
|
|
1264
1731
|
enabled=enabled,
|
|
1265
|
-
annotations=
|
|
1732
|
+
annotations=annotations,
|
|
1266
1733
|
meta=meta,
|
|
1267
1734
|
)
|
|
1268
1735
|
self.add_template(template)
|
|
@@ -1274,10 +1741,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1274
1741
|
name=name,
|
|
1275
1742
|
title=title,
|
|
1276
1743
|
description=description,
|
|
1744
|
+
icons=icons,
|
|
1277
1745
|
mime_type=mime_type,
|
|
1278
1746
|
tags=tags,
|
|
1279
1747
|
enabled=enabled,
|
|
1280
|
-
annotations=
|
|
1748
|
+
annotations=annotations,
|
|
1281
1749
|
meta=meta,
|
|
1282
1750
|
)
|
|
1283
1751
|
self.add_resource(resource)
|
|
@@ -1320,6 +1788,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1320
1788
|
name: str | None = None,
|
|
1321
1789
|
title: str | None = None,
|
|
1322
1790
|
description: str | None = None,
|
|
1791
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1323
1792
|
tags: set[str] | None = None,
|
|
1324
1793
|
enabled: bool | None = None,
|
|
1325
1794
|
meta: dict[str, Any] | None = None,
|
|
@@ -1333,6 +1802,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1333
1802
|
name: str | None = None,
|
|
1334
1803
|
title: str | None = None,
|
|
1335
1804
|
description: str | None = None,
|
|
1805
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1336
1806
|
tags: set[str] | None = None,
|
|
1337
1807
|
enabled: bool | None = None,
|
|
1338
1808
|
meta: dict[str, Any] | None = None,
|
|
@@ -1345,6 +1815,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1345
1815
|
name: str | None = None,
|
|
1346
1816
|
title: str | None = None,
|
|
1347
1817
|
description: str | None = None,
|
|
1818
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1348
1819
|
tags: set[str] | None = None,
|
|
1349
1820
|
enabled: bool | None = None,
|
|
1350
1821
|
meta: dict[str, Any] | None = None,
|
|
@@ -1384,8 +1855,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1384
1855
|
]
|
|
1385
1856
|
|
|
1386
1857
|
@server.prompt()
|
|
1387
|
-
def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
|
1388
|
-
ctx.info(f"Analyzing table {table_name}")
|
|
1858
|
+
async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
|
1859
|
+
await ctx.info(f"Analyzing table {table_name}")
|
|
1389
1860
|
schema = read_table_schema(table_name)
|
|
1390
1861
|
return [
|
|
1391
1862
|
{
|
|
@@ -1395,7 +1866,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1395
1866
|
]
|
|
1396
1867
|
|
|
1397
1868
|
@server.prompt("custom_name")
|
|
1398
|
-
def analyze_file(path: str) -> list[Message]:
|
|
1869
|
+
async def analyze_file(path: str) -> list[Message]:
|
|
1399
1870
|
content = await read_file(path)
|
|
1400
1871
|
return [
|
|
1401
1872
|
{
|
|
@@ -1444,6 +1915,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1444
1915
|
name=prompt_name,
|
|
1445
1916
|
title=title,
|
|
1446
1917
|
description=description,
|
|
1918
|
+
icons=icons,
|
|
1447
1919
|
tags=tags,
|
|
1448
1920
|
enabled=enabled,
|
|
1449
1921
|
meta=meta,
|
|
@@ -1474,14 +1946,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1474
1946
|
name=prompt_name,
|
|
1475
1947
|
title=title,
|
|
1476
1948
|
description=description,
|
|
1949
|
+
icons=icons,
|
|
1477
1950
|
tags=tags,
|
|
1478
1951
|
enabled=enabled,
|
|
1479
1952
|
meta=meta,
|
|
1480
1953
|
)
|
|
1481
1954
|
|
|
1482
|
-
async def run_stdio_async(
|
|
1483
|
-
|
|
1955
|
+
async def run_stdio_async(
|
|
1956
|
+
self, show_banner: bool = True, log_level: str | None = None
|
|
1957
|
+
) -> None:
|
|
1958
|
+
"""Run the server using stdio transport.
|
|
1484
1959
|
|
|
1960
|
+
Args:
|
|
1961
|
+
show_banner: Whether to display the server banner
|
|
1962
|
+
log_level: Log level for the server
|
|
1963
|
+
"""
|
|
1485
1964
|
# Display server banner
|
|
1486
1965
|
if show_banner:
|
|
1487
1966
|
log_server_banner(
|
|
@@ -1489,15 +1968,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1489
1968
|
transport="stdio",
|
|
1490
1969
|
)
|
|
1491
1970
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1971
|
+
with temporary_log_level(log_level):
|
|
1972
|
+
async with self._lifespan_manager():
|
|
1973
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
1974
|
+
logger.info(
|
|
1975
|
+
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
1976
|
+
)
|
|
1977
|
+
await self._mcp_server.run(
|
|
1978
|
+
read_stream,
|
|
1979
|
+
write_stream,
|
|
1980
|
+
self._mcp_server.create_initialization_options(
|
|
1981
|
+
NotificationOptions(tools_changed=True)
|
|
1982
|
+
),
|
|
1983
|
+
)
|
|
1501
1984
|
|
|
1502
1985
|
async def run_http_async(
|
|
1503
1986
|
self,
|
|
@@ -1509,6 +1992,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1509
1992
|
path: str | None = None,
|
|
1510
1993
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1511
1994
|
middleware: list[ASGIMiddleware] | None = None,
|
|
1995
|
+
json_response: bool | None = None,
|
|
1512
1996
|
stateless_http: bool | None = None,
|
|
1513
1997
|
) -> None:
|
|
1514
1998
|
"""Run the server using HTTP transport.
|
|
@@ -1521,9 +2005,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1521
2005
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
1522
2006
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
1523
2007
|
middleware: A list of middleware to apply to the app
|
|
2008
|
+
json_response: Whether to use JSON response format (defaults to settings.json_response)
|
|
1524
2009
|
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
|
1525
2010
|
"""
|
|
1526
|
-
|
|
1527
2011
|
host = host or self._deprecated_settings.host
|
|
1528
2012
|
port = port or self._deprecated_settings.port
|
|
1529
2013
|
default_log_level_to_use = (
|
|
@@ -1534,6 +2018,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1534
2018
|
path=path,
|
|
1535
2019
|
transport=transport,
|
|
1536
2020
|
middleware=middleware,
|
|
2021
|
+
json_response=json_response,
|
|
1537
2022
|
stateless_http=stateless_http,
|
|
1538
2023
|
)
|
|
1539
2024
|
|
|
@@ -1553,25 +2038,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1553
2038
|
port=port,
|
|
1554
2039
|
path=server_path,
|
|
1555
2040
|
)
|
|
1556
|
-
|
|
2041
|
+
uvicorn_config_from_user = uvicorn_config or {}
|
|
1557
2042
|
|
|
1558
2043
|
config_kwargs: dict[str, Any] = {
|
|
1559
2044
|
"timeout_graceful_shutdown": 0,
|
|
1560
2045
|
"lifespan": "on",
|
|
2046
|
+
"ws": "websockets-sansio",
|
|
1561
2047
|
}
|
|
1562
|
-
config_kwargs.update(
|
|
2048
|
+
config_kwargs.update(uvicorn_config_from_user)
|
|
1563
2049
|
|
|
1564
2050
|
if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
|
|
1565
2051
|
config_kwargs["log_level"] = default_log_level_to_use
|
|
1566
2052
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
2053
|
+
with temporary_log_level(log_level):
|
|
2054
|
+
async with self._lifespan_manager():
|
|
2055
|
+
config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
|
|
2056
|
+
server = uvicorn.Server(config)
|
|
2057
|
+
path = app.state.path.lstrip("/") # type: ignore
|
|
2058
|
+
logger.info(
|
|
2059
|
+
f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
|
|
2060
|
+
)
|
|
1573
2061
|
|
|
1574
|
-
|
|
2062
|
+
await server.serve()
|
|
1575
2063
|
|
|
1576
2064
|
async def run_sse_async(
|
|
1577
2065
|
self,
|
|
@@ -1833,7 +2321,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1833
2321
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1834
2322
|
# we should treat it as a proxy
|
|
1835
2323
|
if as_proxy is None:
|
|
1836
|
-
as_proxy = server.
|
|
2324
|
+
as_proxy = server._lifespan != default_lifespan
|
|
1837
2325
|
|
|
1838
2326
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1839
2327
|
server = FastMCP.as_proxy(server)
|
|
@@ -1845,9 +2333,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1845
2333
|
resource_prefix_format=self.resource_prefix_format,
|
|
1846
2334
|
)
|
|
1847
2335
|
self._mounted_servers.append(mounted_server)
|
|
1848
|
-
self._tool_manager.mount(mounted_server)
|
|
1849
|
-
self._resource_manager.mount(mounted_server)
|
|
1850
|
-
self._prompt_manager.mount(mounted_server)
|
|
1851
2336
|
|
|
1852
2337
|
async def import_server(
|
|
1853
2338
|
self,
|
|
@@ -1970,6 +2455,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1970
2455
|
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
|
1971
2456
|
self._prompt_manager.add_prompt(prompt)
|
|
1972
2457
|
|
|
2458
|
+
if server._lifespan != default_lifespan:
|
|
2459
|
+
from warnings import warn
|
|
2460
|
+
|
|
2461
|
+
warn(
|
|
2462
|
+
message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
|
|
2463
|
+
category=RuntimeWarning,
|
|
2464
|
+
stacklevel=2,
|
|
2465
|
+
)
|
|
2466
|
+
|
|
1973
2467
|
if prefix:
|
|
1974
2468
|
logger.debug(
|
|
1975
2469
|
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|
|
@@ -2096,6 +2590,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2096
2590
|
Client[ClientTransportT]
|
|
2097
2591
|
| ClientTransport
|
|
2098
2592
|
| FastMCP[Any]
|
|
2593
|
+
| FastMCP1Server
|
|
2099
2594
|
| AnyUrl
|
|
2100
2595
|
| Path
|
|
2101
2596
|
| MCPConfig
|
|
@@ -2120,10 +2615,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2120
2615
|
# - Connected clients: reuse existing session for all requests
|
|
2121
2616
|
# - Disconnected clients: create fresh sessions per request for isolation
|
|
2122
2617
|
if client.is_connected():
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
logger = get_logger(__name__)
|
|
2126
|
-
logger.info(
|
|
2618
|
+
proxy_logger = get_logger(__name__)
|
|
2619
|
+
proxy_logger.info(
|
|
2127
2620
|
"Proxy detected connected client - reusing existing session for all requests. "
|
|
2128
2621
|
"This may cause context mixing in concurrent scenarios."
|
|
2129
2622
|
)
|
|
@@ -2140,7 +2633,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2140
2633
|
|
|
2141
2634
|
client_factory = fresh_client_factory
|
|
2142
2635
|
else:
|
|
2143
|
-
base_client = ProxyClient(backend)
|
|
2636
|
+
base_client = ProxyClient(backend) # type: ignore
|
|
2144
2637
|
|
|
2145
2638
|
# Fresh client created from transport - use fresh sessions per request
|
|
2146
2639
|
def proxy_client_factory():
|
|
@@ -2195,10 +2688,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2195
2688
|
return False
|
|
2196
2689
|
|
|
2197
2690
|
if self.include_tags is not None:
|
|
2198
|
-
|
|
2199
|
-
return True
|
|
2200
|
-
else:
|
|
2201
|
-
return False
|
|
2691
|
+
return bool(any(itag in component.tags for itag in self.include_tags))
|
|
2202
2692
|
|
|
2203
2693
|
return True
|
|
2204
2694
|
|