fastmcp 2.12.5__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 +11 -11
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- 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 +14 -15
- 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 +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- 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 +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- 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 +35 -17
- fastmcp/server/context.py +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- 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 +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- 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 +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -7,8 +7,19 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import secrets
|
|
9
9
|
import warnings
|
|
10
|
-
from collections.abc import
|
|
11
|
-
|
|
10
|
+
from collections.abc import (
|
|
11
|
+
AsyncIterator,
|
|
12
|
+
Awaitable,
|
|
13
|
+
Callable,
|
|
14
|
+
Collection,
|
|
15
|
+
Mapping,
|
|
16
|
+
Sequence,
|
|
17
|
+
)
|
|
18
|
+
from contextlib import (
|
|
19
|
+
AbstractAsyncContextManager,
|
|
20
|
+
AsyncExitStack,
|
|
21
|
+
asynccontextmanager,
|
|
22
|
+
)
|
|
12
23
|
from dataclasses import dataclass
|
|
13
24
|
from functools import partial
|
|
14
25
|
from pathlib import Path
|
|
@@ -43,9 +54,11 @@ import fastmcp
|
|
|
43
54
|
import fastmcp.server
|
|
44
55
|
from fastmcp.exceptions import DisabledError, NotFoundError
|
|
45
56
|
from fastmcp.mcp_config import MCPConfig
|
|
46
|
-
from fastmcp.prompts import Prompt
|
|
57
|
+
from fastmcp.prompts import Prompt
|
|
47
58
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
48
|
-
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
|
|
49
62
|
from fastmcp.resources.template import ResourceTemplate
|
|
50
63
|
from fastmcp.server.auth import AuthProvider
|
|
51
64
|
from fastmcp.server.http import (
|
|
@@ -56,8 +69,8 @@ from fastmcp.server.http import (
|
|
|
56
69
|
from fastmcp.server.low_level import LowLevelServer
|
|
57
70
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
58
71
|
from fastmcp.settings import Settings
|
|
59
|
-
from fastmcp.tools import ToolManager
|
|
60
72
|
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
73
|
+
from fastmcp.tools.tool_manager import ToolManager
|
|
61
74
|
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
62
75
|
from fastmcp.utilities.cli import log_server_banner
|
|
63
76
|
from fastmcp.utilities.components import FastMCPComponent
|
|
@@ -66,7 +79,7 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
66
79
|
|
|
67
80
|
if TYPE_CHECKING:
|
|
68
81
|
from fastmcp.client import Client
|
|
69
|
-
from fastmcp.client.
|
|
82
|
+
from fastmcp.client.client import FastMCP1Server
|
|
70
83
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
71
84
|
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
72
85
|
from fastmcp.experimental.server.openapi.routing import (
|
|
@@ -80,6 +93,8 @@ if TYPE_CHECKING:
|
|
|
80
93
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
81
94
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
82
95
|
from fastmcp.server.proxy import FastMCPProxy
|
|
96
|
+
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
97
|
+
from fastmcp.tools.tool import ToolResultSerializerType
|
|
83
98
|
|
|
84
99
|
logger = get_logger(__name__)
|
|
85
100
|
|
|
@@ -89,6 +104,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
|
89
104
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
90
105
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
91
106
|
|
|
107
|
+
LifespanCallable = Callable[
|
|
108
|
+
["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
|
|
109
|
+
]
|
|
110
|
+
|
|
92
111
|
|
|
93
112
|
@asynccontextmanager
|
|
94
113
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -98,26 +117,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
|
|
|
98
117
|
server: The server instance this lifespan is managing
|
|
99
118
|
|
|
100
119
|
Returns:
|
|
101
|
-
An empty
|
|
120
|
+
An empty dictionary as the lifespan result.
|
|
102
121
|
"""
|
|
103
122
|
yield {}
|
|
104
123
|
|
|
105
124
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
lifespan: Callable[
|
|
109
|
-
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
110
|
-
],
|
|
125
|
+
def _lifespan_proxy(
|
|
126
|
+
fastmcp_server: FastMCP[LifespanResultT],
|
|
111
127
|
) -> Callable[
|
|
112
128
|
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
113
129
|
]:
|
|
114
130
|
@asynccontextmanager
|
|
115
131
|
async def wrap(
|
|
116
|
-
|
|
132
|
+
low_level_server: LowLevelServer[LifespanResultT],
|
|
117
133
|
) -> AsyncIterator[LifespanResultT]:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
121
145
|
|
|
122
146
|
return wrap
|
|
123
147
|
|
|
@@ -129,27 +153,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
129
153
|
instructions: str | None = None,
|
|
130
154
|
*,
|
|
131
155
|
version: str | None = None,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
AbstractAsyncContextManager[LifespanResultT],
|
|
138
|
-
]
|
|
139
|
-
| None
|
|
140
|
-
) = 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,
|
|
141
161
|
dependencies: list[str] | None = None,
|
|
142
162
|
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
143
163
|
mask_error_details: bool | None = None,
|
|
144
|
-
tools:
|
|
145
|
-
tool_transformations:
|
|
146
|
-
tool_serializer:
|
|
147
|
-
include_tags:
|
|
148
|
-
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,
|
|
149
169
|
include_fastmcp_meta: bool | None = None,
|
|
150
170
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
151
171
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
152
172
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
173
|
+
strict_input_validation: bool | None = None,
|
|
153
174
|
# ---
|
|
154
175
|
# ---
|
|
155
176
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -173,32 +194,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
173
194
|
|
|
174
195
|
self._additional_http_routes: list[BaseRoute] = []
|
|
175
196
|
self._mounted_servers: list[MountedServer] = []
|
|
176
|
-
self._tool_manager = ToolManager(
|
|
197
|
+
self._tool_manager: ToolManager = ToolManager(
|
|
177
198
|
duplicate_behavior=on_duplicate_tools,
|
|
178
199
|
mask_error_details=mask_error_details,
|
|
179
200
|
transformations=tool_transformations,
|
|
180
201
|
)
|
|
181
|
-
self._resource_manager = ResourceManager(
|
|
202
|
+
self._resource_manager: ResourceManager = ResourceManager(
|
|
182
203
|
duplicate_behavior=on_duplicate_resources,
|
|
183
204
|
mask_error_details=mask_error_details,
|
|
184
205
|
)
|
|
185
|
-
self._prompt_manager = PromptManager(
|
|
206
|
+
self._prompt_manager: PromptManager = PromptManager(
|
|
186
207
|
duplicate_behavior=on_duplicate_prompts,
|
|
187
208
|
mask_error_details=mask_error_details,
|
|
188
209
|
)
|
|
189
|
-
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
|
|
190
215
|
|
|
191
|
-
if lifespan is None:
|
|
192
|
-
self._has_lifespan = False
|
|
193
|
-
lifespan = default_lifespan
|
|
194
|
-
else:
|
|
195
|
-
self._has_lifespan = True
|
|
196
216
|
# Generate random ID if no name provided
|
|
197
|
-
self._mcp_server = LowLevelServer[
|
|
217
|
+
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
|
|
218
|
+
LifespanResultT
|
|
219
|
+
](
|
|
220
|
+
fastmcp=self,
|
|
198
221
|
name=name or self.generate_name(),
|
|
199
|
-
version=version,
|
|
222
|
+
version=version or fastmcp.__version__,
|
|
200
223
|
instructions=instructions,
|
|
201
|
-
|
|
224
|
+
website_url=website_url,
|
|
225
|
+
icons=icons,
|
|
226
|
+
lifespan=_lifespan_proxy(fastmcp_server=self),
|
|
202
227
|
)
|
|
203
228
|
|
|
204
229
|
# if auth is `NotSet`, try to create a provider from the environment
|
|
@@ -208,7 +233,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
208
233
|
auth = fastmcp.settings.server_auth_class()
|
|
209
234
|
else:
|
|
210
235
|
auth = None
|
|
211
|
-
self.auth = cast(AuthProvider | None, auth)
|
|
236
|
+
self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
|
|
212
237
|
|
|
213
238
|
if tools:
|
|
214
239
|
for tool in tools:
|
|
@@ -216,10 +241,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
216
241
|
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
|
217
242
|
self.add_tool(tool)
|
|
218
243
|
|
|
219
|
-
self.include_tags =
|
|
220
|
-
|
|
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
|
+
)
|
|
221
250
|
|
|
222
|
-
self.
|
|
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
|
+
)
|
|
256
|
+
|
|
257
|
+
self.middleware: list[Middleware] = list(middleware or [])
|
|
223
258
|
|
|
224
259
|
# Set up MCP protocol handlers
|
|
225
260
|
self._setup_handlers()
|
|
@@ -238,14 +273,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
238
273
|
DeprecationWarning,
|
|
239
274
|
stacklevel=2,
|
|
240
275
|
)
|
|
241
|
-
self.dependencies = (
|
|
276
|
+
self.dependencies: list[str] = (
|
|
242
277
|
dependencies or fastmcp.settings.server_dependencies
|
|
243
278
|
) # TODO: Remove (deprecated in v2.11.4)
|
|
244
279
|
|
|
245
|
-
self.sampling_handler =
|
|
246
|
-
|
|
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
|
+
)
|
|
247
286
|
|
|
248
|
-
self.include_fastmcp_meta = (
|
|
287
|
+
self.include_fastmcp_meta: bool = (
|
|
249
288
|
include_fastmcp_meta
|
|
250
289
|
if include_fastmcp_meta is not None
|
|
251
290
|
else fastmcp.settings.include_fastmcp_meta
|
|
@@ -333,6 +372,38 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
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
|
+
)
|
|
565
721
|
|
|
566
|
-
async def
|
|
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())
|
|
766
|
+
|
|
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
|
+
)
|
|
899
|
+
|
|
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
|
+
}
|
|
655
921
|
|
|
656
|
-
|
|
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
|
+
)
|
|
1098
|
+
|
|
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 :]
|
|
753
1115
|
|
|
754
|
-
|
|
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
|
+
)
|
|
1184
|
+
|
|
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
|
|
806
1216
|
|
|
807
|
-
|
|
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
|
"""
|
|
@@ -820,32 +1246,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
820
1246
|
|
|
821
1247
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
822
1248
|
try:
|
|
823
|
-
return await self.
|
|
824
|
-
except DisabledError:
|
|
1249
|
+
return await self._get_prompt_middleware(name, arguments)
|
|
1250
|
+
except DisabledError as e:
|
|
825
1251
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
826
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
827
|
-
except NotFoundError:
|
|
1252
|
+
raise NotFoundError(f"Unknown prompt: {name}") from e
|
|
1253
|
+
except NotFoundError as e:
|
|
828
1254
|
# standardize NotFound message
|
|
829
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
1255
|
+
raise NotFoundError(f"Unknown prompt: {name}") from e
|
|
830
1256
|
|
|
831
|
-
async def
|
|
1257
|
+
async def _get_prompt_middleware(
|
|
832
1258
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
833
1259
|
) -> GetPromptResult:
|
|
834
1260
|
"""
|
|
835
1261
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
836
1262
|
"""
|
|
837
1263
|
|
|
838
|
-
async def _handler(
|
|
839
|
-
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
840
|
-
) -> GetPromptResult:
|
|
841
|
-
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
|
842
|
-
if not self._should_enable_component(prompt):
|
|
843
|
-
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
|
844
|
-
|
|
845
|
-
return await self._prompt_manager.render_prompt(
|
|
846
|
-
name=context.message.name, arguments=context.message.arguments
|
|
847
|
-
)
|
|
848
|
-
|
|
849
1264
|
mw_context = MiddlewareContext(
|
|
850
1265
|
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
851
1266
|
source="client",
|
|
@@ -853,7 +1268,47 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
853
1268
|
method="prompts/get",
|
|
854
1269
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
855
1270
|
)
|
|
856
|
-
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}")
|
|
857
1312
|
|
|
858
1313
|
def add_tool(self, tool: Tool) -> Tool:
|
|
859
1314
|
"""Add a tool to the server.
|
|
@@ -918,8 +1373,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
918
1373
|
name: str | None = None,
|
|
919
1374
|
title: str | None = None,
|
|
920
1375
|
description: str | None = None,
|
|
1376
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
921
1377
|
tags: set[str] | None = None,
|
|
922
|
-
output_schema: dict[str, Any] |
|
|
1378
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
923
1379
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
924
1380
|
exclude_args: list[str] | None = None,
|
|
925
1381
|
meta: dict[str, Any] | None = None,
|
|
@@ -934,8 +1390,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
934
1390
|
name: str | None = None,
|
|
935
1391
|
title: str | None = None,
|
|
936
1392
|
description: str | None = None,
|
|
1393
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
937
1394
|
tags: set[str] | None = None,
|
|
938
|
-
output_schema: dict[str, Any] |
|
|
1395
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
939
1396
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
940
1397
|
exclude_args: list[str] | None = None,
|
|
941
1398
|
meta: dict[str, Any] | None = None,
|
|
@@ -949,8 +1406,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
949
1406
|
name: str | None = None,
|
|
950
1407
|
title: str | None = None,
|
|
951
1408
|
description: str | None = None,
|
|
1409
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
952
1410
|
tags: set[str] | None = None,
|
|
953
|
-
output_schema: dict[str, Any] |
|
|
1411
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
954
1412
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
955
1413
|
exclude_args: list[str] | None = None,
|
|
956
1414
|
meta: dict[str, Any] | None = None,
|
|
@@ -976,7 +1434,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
976
1434
|
tags: Optional set of tags for categorizing the tool
|
|
977
1435
|
output_schema: Optional JSON schema for the tool's output
|
|
978
1436
|
annotations: Optional annotations about the tool's behavior
|
|
979
|
-
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.
|
|
980
1440
|
meta: Optional meta information about the tool
|
|
981
1441
|
enabled: Optional boolean to enable or disable the tool
|
|
982
1442
|
|
|
@@ -1027,14 +1487,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1027
1487
|
tool_name = name # Use keyword name if provided, otherwise None
|
|
1028
1488
|
|
|
1029
1489
|
# Register the tool immediately and return the tool object
|
|
1490
|
+
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
|
1030
1491
|
tool = Tool.from_function(
|
|
1031
1492
|
fn,
|
|
1032
1493
|
name=tool_name,
|
|
1033
1494
|
title=title,
|
|
1034
1495
|
description=description,
|
|
1496
|
+
icons=icons,
|
|
1035
1497
|
tags=tags,
|
|
1036
1498
|
output_schema=output_schema,
|
|
1037
|
-
annotations=
|
|
1499
|
+
annotations=annotations,
|
|
1038
1500
|
exclude_args=exclude_args,
|
|
1039
1501
|
meta=meta,
|
|
1040
1502
|
serializer=self._tool_serializer,
|
|
@@ -1065,6 +1527,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1065
1527
|
name=tool_name,
|
|
1066
1528
|
title=title,
|
|
1067
1529
|
description=description,
|
|
1530
|
+
icons=icons,
|
|
1068
1531
|
tags=tags,
|
|
1069
1532
|
output_schema=output_schema,
|
|
1070
1533
|
annotations=annotations,
|
|
@@ -1162,6 +1625,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1162
1625
|
name: str | None = None,
|
|
1163
1626
|
title: str | None = None,
|
|
1164
1627
|
description: str | None = None,
|
|
1628
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1165
1629
|
mime_type: str | None = None,
|
|
1166
1630
|
tags: set[str] | None = None,
|
|
1167
1631
|
enabled: bool | None = None,
|
|
@@ -1261,10 +1725,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1261
1725
|
name=name,
|
|
1262
1726
|
title=title,
|
|
1263
1727
|
description=description,
|
|
1728
|
+
icons=icons,
|
|
1264
1729
|
mime_type=mime_type,
|
|
1265
1730
|
tags=tags,
|
|
1266
1731
|
enabled=enabled,
|
|
1267
|
-
annotations=
|
|
1732
|
+
annotations=annotations,
|
|
1268
1733
|
meta=meta,
|
|
1269
1734
|
)
|
|
1270
1735
|
self.add_template(template)
|
|
@@ -1276,10 +1741,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1276
1741
|
name=name,
|
|
1277
1742
|
title=title,
|
|
1278
1743
|
description=description,
|
|
1744
|
+
icons=icons,
|
|
1279
1745
|
mime_type=mime_type,
|
|
1280
1746
|
tags=tags,
|
|
1281
1747
|
enabled=enabled,
|
|
1282
|
-
annotations=
|
|
1748
|
+
annotations=annotations,
|
|
1283
1749
|
meta=meta,
|
|
1284
1750
|
)
|
|
1285
1751
|
self.add_resource(resource)
|
|
@@ -1322,6 +1788,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1322
1788
|
name: str | None = None,
|
|
1323
1789
|
title: str | None = None,
|
|
1324
1790
|
description: str | None = None,
|
|
1791
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1325
1792
|
tags: set[str] | None = None,
|
|
1326
1793
|
enabled: bool | None = None,
|
|
1327
1794
|
meta: dict[str, Any] | None = None,
|
|
@@ -1335,6 +1802,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1335
1802
|
name: str | None = None,
|
|
1336
1803
|
title: str | None = None,
|
|
1337
1804
|
description: str | None = None,
|
|
1805
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1338
1806
|
tags: set[str] | None = None,
|
|
1339
1807
|
enabled: bool | None = None,
|
|
1340
1808
|
meta: dict[str, Any] | None = None,
|
|
@@ -1347,6 +1815,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1347
1815
|
name: str | None = None,
|
|
1348
1816
|
title: str | None = None,
|
|
1349
1817
|
description: str | None = None,
|
|
1818
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1350
1819
|
tags: set[str] | None = None,
|
|
1351
1820
|
enabled: bool | None = None,
|
|
1352
1821
|
meta: dict[str, Any] | None = None,
|
|
@@ -1446,6 +1915,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1446
1915
|
name=prompt_name,
|
|
1447
1916
|
title=title,
|
|
1448
1917
|
description=description,
|
|
1918
|
+
icons=icons,
|
|
1449
1919
|
tags=tags,
|
|
1450
1920
|
enabled=enabled,
|
|
1451
1921
|
meta=meta,
|
|
@@ -1476,6 +1946,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1476
1946
|
name=prompt_name,
|
|
1477
1947
|
title=title,
|
|
1478
1948
|
description=description,
|
|
1949
|
+
icons=icons,
|
|
1479
1950
|
tags=tags,
|
|
1480
1951
|
enabled=enabled,
|
|
1481
1952
|
meta=meta,
|
|
@@ -1498,15 +1969,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1498
1969
|
)
|
|
1499
1970
|
|
|
1500
1971
|
with temporary_log_level(log_level):
|
|
1501
|
-
async with
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
self._mcp_server.
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
+
)
|
|
1510
1984
|
|
|
1511
1985
|
async def run_http_async(
|
|
1512
1986
|
self,
|
|
@@ -1518,6 +1992,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1518
1992
|
path: str | None = None,
|
|
1519
1993
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1520
1994
|
middleware: list[ASGIMiddleware] | None = None,
|
|
1995
|
+
json_response: bool | None = None,
|
|
1521
1996
|
stateless_http: bool | None = None,
|
|
1522
1997
|
) -> None:
|
|
1523
1998
|
"""Run the server using HTTP transport.
|
|
@@ -1530,6 +2005,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1530
2005
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
1531
2006
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
1532
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)
|
|
1533
2009
|
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
|
1534
2010
|
"""
|
|
1535
2011
|
host = host or self._deprecated_settings.host
|
|
@@ -1542,6 +2018,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1542
2018
|
path=path,
|
|
1543
2019
|
transport=transport,
|
|
1544
2020
|
middleware=middleware,
|
|
2021
|
+
json_response=json_response,
|
|
1545
2022
|
stateless_http=stateless_http,
|
|
1546
2023
|
)
|
|
1547
2024
|
|
|
@@ -1561,26 +2038,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1561
2038
|
port=port,
|
|
1562
2039
|
path=server_path,
|
|
1563
2040
|
)
|
|
1564
|
-
|
|
2041
|
+
uvicorn_config_from_user = uvicorn_config or {}
|
|
1565
2042
|
|
|
1566
2043
|
config_kwargs: dict[str, Any] = {
|
|
1567
2044
|
"timeout_graceful_shutdown": 0,
|
|
1568
2045
|
"lifespan": "on",
|
|
2046
|
+
"ws": "websockets-sansio",
|
|
1569
2047
|
}
|
|
1570
|
-
config_kwargs.update(
|
|
2048
|
+
config_kwargs.update(uvicorn_config_from_user)
|
|
1571
2049
|
|
|
1572
2050
|
if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
|
|
1573
2051
|
config_kwargs["log_level"] = default_log_level_to_use
|
|
1574
2052
|
|
|
1575
2053
|
with temporary_log_level(log_level):
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
+
)
|
|
1582
2061
|
|
|
1583
|
-
|
|
2062
|
+
await server.serve()
|
|
1584
2063
|
|
|
1585
2064
|
async def run_sse_async(
|
|
1586
2065
|
self,
|
|
@@ -1842,7 +2321,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1842
2321
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1843
2322
|
# we should treat it as a proxy
|
|
1844
2323
|
if as_proxy is None:
|
|
1845
|
-
as_proxy = server.
|
|
2324
|
+
as_proxy = server._lifespan != default_lifespan
|
|
1846
2325
|
|
|
1847
2326
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1848
2327
|
server = FastMCP.as_proxy(server)
|
|
@@ -1854,9 +2333,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1854
2333
|
resource_prefix_format=self.resource_prefix_format,
|
|
1855
2334
|
)
|
|
1856
2335
|
self._mounted_servers.append(mounted_server)
|
|
1857
|
-
self._tool_manager.mount(mounted_server)
|
|
1858
|
-
self._resource_manager.mount(mounted_server)
|
|
1859
|
-
self._prompt_manager.mount(mounted_server)
|
|
1860
2336
|
|
|
1861
2337
|
async def import_server(
|
|
1862
2338
|
self,
|
|
@@ -1979,6 +2455,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1979
2455
|
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
|
1980
2456
|
self._prompt_manager.add_prompt(prompt)
|
|
1981
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
|
+
|
|
1982
2467
|
if prefix:
|
|
1983
2468
|
logger.debug(
|
|
1984
2469
|
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|
|
@@ -2105,6 +2590,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2105
2590
|
Client[ClientTransportT]
|
|
2106
2591
|
| ClientTransport
|
|
2107
2592
|
| FastMCP[Any]
|
|
2593
|
+
| FastMCP1Server
|
|
2108
2594
|
| AnyUrl
|
|
2109
2595
|
| Path
|
|
2110
2596
|
| MCPConfig
|
|
@@ -2129,8 +2615,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2129
2615
|
# - Connected clients: reuse existing session for all requests
|
|
2130
2616
|
# - Disconnected clients: create fresh sessions per request for isolation
|
|
2131
2617
|
if client.is_connected():
|
|
2132
|
-
|
|
2133
|
-
|
|
2618
|
+
proxy_logger = get_logger(__name__)
|
|
2619
|
+
proxy_logger.info(
|
|
2134
2620
|
"Proxy detected connected client - reusing existing session for all requests. "
|
|
2135
2621
|
"This may cause context mixing in concurrent scenarios."
|
|
2136
2622
|
)
|
|
@@ -2147,7 +2633,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2147
2633
|
|
|
2148
2634
|
client_factory = fresh_client_factory
|
|
2149
2635
|
else:
|
|
2150
|
-
base_client = ProxyClient(backend)
|
|
2636
|
+
base_client = ProxyClient(backend) # type: ignore
|
|
2151
2637
|
|
|
2152
2638
|
# Fresh client created from transport - use fresh sessions per request
|
|
2153
2639
|
def proxy_client_factory():
|
|
@@ -2202,10 +2688,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2202
2688
|
return False
|
|
2203
2689
|
|
|
2204
2690
|
if self.include_tags is not None:
|
|
2205
|
-
|
|
2206
|
-
return True
|
|
2207
|
-
else:
|
|
2208
|
-
return False
|
|
2691
|
+
return bool(any(itag in component.tags for itag in self.include_tags))
|
|
2209
2692
|
|
|
2210
2693
|
return True
|
|
2211
2694
|
|