fastmcp 2.7.1__py3-none-any.whl → 2.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +4 -1
- fastmcp/cli/cli.py +3 -2
- fastmcp/client/auth/oauth.py +1 -1
- fastmcp/client/client.py +3 -1
- fastmcp/client/transports.py +35 -28
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/prompt.py +8 -18
- fastmcp/prompts/prompt_manager.py +7 -4
- fastmcp/resources/resource.py +21 -26
- fastmcp/resources/resource_manager.py +3 -2
- fastmcp/resources/template.py +8 -16
- fastmcp/server/auth/providers/bearer_env.py +8 -11
- fastmcp/server/openapi.py +65 -38
- fastmcp/server/proxy.py +27 -14
- fastmcp/server/server.py +320 -131
- fastmcp/settings.py +100 -37
- fastmcp/tools/__init__.py +2 -1
- fastmcp/tools/tool.py +114 -75
- fastmcp/tools/tool_manager.py +3 -2
- fastmcp/tools/tool_transform.py +665 -0
- fastmcp/utilities/components.py +55 -0
- fastmcp/utilities/exceptions.py +1 -1
- fastmcp/utilities/mcp_config.py +1 -1
- fastmcp/utilities/tests.py +3 -3
- fastmcp/utilities/types.py +0 -9
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/METADATA +3 -1
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/RECORD +30 -28
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -43,8 +43,7 @@ from starlette.routing import BaseRoute, Route
|
|
|
43
43
|
|
|
44
44
|
import fastmcp
|
|
45
45
|
import fastmcp.server
|
|
46
|
-
|
|
47
|
-
from fastmcp.exceptions import NotFoundError
|
|
46
|
+
from fastmcp.exceptions import DisabledError, NotFoundError
|
|
48
47
|
from fastmcp.prompts import Prompt, PromptManager
|
|
49
48
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
50
49
|
from fastmcp.resources import Resource, ResourceManager
|
|
@@ -56,9 +55,11 @@ from fastmcp.server.http import (
|
|
|
56
55
|
create_sse_app,
|
|
57
56
|
create_streamable_http_app,
|
|
58
57
|
)
|
|
58
|
+
from fastmcp.settings import Settings
|
|
59
59
|
from fastmcp.tools import ToolManager
|
|
60
60
|
from fastmcp.tools.tool import FunctionTool, Tool
|
|
61
61
|
from fastmcp.utilities.cache import TimedCache
|
|
62
|
+
from fastmcp.utilities.components import FastMCPComponent
|
|
62
63
|
from fastmcp.utilities.logging import get_logger
|
|
63
64
|
from fastmcp.utilities.mcp_config import MCPConfig
|
|
64
65
|
|
|
@@ -120,8 +121,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
120
121
|
]
|
|
121
122
|
| None
|
|
122
123
|
) = None,
|
|
123
|
-
tags: set[str] | None = None,
|
|
124
|
-
dependencies: list[str] | None = None,
|
|
125
124
|
tool_serializer: Callable[[Any], str] | None = None,
|
|
126
125
|
cache_expiration_seconds: float | None = None,
|
|
127
126
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
@@ -130,44 +129,44 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
130
129
|
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
131
130
|
mask_error_details: bool | None = None,
|
|
132
131
|
tools: list[Tool | Callable[..., Any]] | None = None,
|
|
133
|
-
|
|
132
|
+
dependencies: list[str] | None = None,
|
|
133
|
+
include_tags: set[str] | None = None,
|
|
134
|
+
exclude_tags: set[str] | None = None,
|
|
135
|
+
# ---
|
|
136
|
+
# ---
|
|
137
|
+
# --- The following arguments are DEPRECATED ---
|
|
138
|
+
# ---
|
|
139
|
+
# ---
|
|
140
|
+
log_level: str | None = None,
|
|
141
|
+
debug: bool | None = None,
|
|
142
|
+
host: str | None = None,
|
|
143
|
+
port: int | None = None,
|
|
144
|
+
sse_path: str | None = None,
|
|
145
|
+
message_path: str | None = None,
|
|
146
|
+
streamable_http_path: str | None = None,
|
|
147
|
+
json_response: bool | None = None,
|
|
148
|
+
stateless_http: bool | None = None,
|
|
134
149
|
):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# If mask_error_details is provided, override the settings value
|
|
140
|
-
if mask_error_details is not None:
|
|
141
|
-
self.settings.mask_error_details = mask_error_details
|
|
142
|
-
|
|
143
|
-
self.resource_prefix_format: Literal["protocol", "path"]
|
|
144
|
-
if resource_prefix_format is None:
|
|
145
|
-
self.resource_prefix_format = (
|
|
146
|
-
fastmcp.settings.settings.resource_prefix_format
|
|
147
|
-
)
|
|
148
|
-
else:
|
|
149
|
-
self.resource_prefix_format = resource_prefix_format
|
|
150
|
+
self.resource_prefix_format: Literal["protocol", "path"] = (
|
|
151
|
+
resource_prefix_format or fastmcp.settings.resource_prefix_format
|
|
152
|
+
)
|
|
150
153
|
|
|
151
|
-
self.tags: set[str] = tags or set()
|
|
152
|
-
self.dependencies = dependencies
|
|
153
154
|
self._cache = TimedCache(
|
|
154
|
-
expiration=datetime.timedelta(
|
|
155
|
-
seconds=self.settings.cache_expiration_seconds
|
|
156
|
-
)
|
|
155
|
+
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
|
157
156
|
)
|
|
158
157
|
self._mounted_servers: dict[str, MountedServer] = {}
|
|
159
158
|
self._additional_http_routes: list[BaseRoute] = []
|
|
160
159
|
self._tool_manager = ToolManager(
|
|
161
160
|
duplicate_behavior=on_duplicate_tools,
|
|
162
|
-
mask_error_details=
|
|
161
|
+
mask_error_details=mask_error_details,
|
|
163
162
|
)
|
|
164
163
|
self._resource_manager = ResourceManager(
|
|
165
164
|
duplicate_behavior=on_duplicate_resources,
|
|
166
|
-
mask_error_details=
|
|
165
|
+
mask_error_details=mask_error_details,
|
|
167
166
|
)
|
|
168
167
|
self._prompt_manager = PromptManager(
|
|
169
168
|
duplicate_behavior=on_duplicate_prompts,
|
|
170
|
-
mask_error_details=
|
|
169
|
+
mask_error_details=mask_error_details,
|
|
171
170
|
)
|
|
172
171
|
self._tool_serializer = tool_serializer
|
|
173
172
|
|
|
@@ -182,7 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
182
181
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
183
182
|
)
|
|
184
183
|
|
|
185
|
-
if auth is None and
|
|
184
|
+
if auth is None and fastmcp.settings.default_auth_provider == "bearer_env":
|
|
186
185
|
auth = EnvBearerAuthProvider()
|
|
187
186
|
self.auth = auth
|
|
188
187
|
|
|
@@ -192,12 +191,67 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
192
191
|
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
|
193
192
|
self.add_tool(tool)
|
|
194
193
|
|
|
194
|
+
self.include_tags = include_tags
|
|
195
|
+
self.exclude_tags = exclude_tags
|
|
196
|
+
|
|
195
197
|
# Set up MCP protocol handlers
|
|
196
198
|
self._setup_handlers()
|
|
199
|
+
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
|
200
|
+
|
|
201
|
+
# handle deprecated settings
|
|
202
|
+
self._handle_deprecated_settings(
|
|
203
|
+
log_level=log_level,
|
|
204
|
+
debug=debug,
|
|
205
|
+
host=host,
|
|
206
|
+
port=port,
|
|
207
|
+
sse_path=sse_path,
|
|
208
|
+
message_path=message_path,
|
|
209
|
+
streamable_http_path=streamable_http_path,
|
|
210
|
+
json_response=json_response,
|
|
211
|
+
stateless_http=stateless_http,
|
|
212
|
+
)
|
|
197
213
|
|
|
198
214
|
def __repr__(self) -> str:
|
|
199
215
|
return f"{type(self).__name__}({self.name!r})"
|
|
200
216
|
|
|
217
|
+
def _handle_deprecated_settings(
|
|
218
|
+
self,
|
|
219
|
+
log_level: str | None,
|
|
220
|
+
debug: bool | None,
|
|
221
|
+
host: str | None,
|
|
222
|
+
port: int | None,
|
|
223
|
+
sse_path: str | None,
|
|
224
|
+
message_path: str | None,
|
|
225
|
+
streamable_http_path: str | None,
|
|
226
|
+
json_response: bool | None,
|
|
227
|
+
stateless_http: bool | None,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Handle deprecated settings. Deprecated in 2.8.0."""
|
|
230
|
+
deprecated_settings: dict[str, Any] = {}
|
|
231
|
+
|
|
232
|
+
for name, arg in [
|
|
233
|
+
("log_level", log_level),
|
|
234
|
+
("debug", debug),
|
|
235
|
+
("host", host),
|
|
236
|
+
("port", port),
|
|
237
|
+
("sse_path", sse_path),
|
|
238
|
+
("message_path", message_path),
|
|
239
|
+
("streamable_http_path", streamable_http_path),
|
|
240
|
+
("json_response", json_response),
|
|
241
|
+
("stateless_http", stateless_http),
|
|
242
|
+
]:
|
|
243
|
+
if arg is not None:
|
|
244
|
+
# Deprecated in 2.8.0
|
|
245
|
+
warnings.warn(
|
|
246
|
+
f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.",
|
|
247
|
+
DeprecationWarning,
|
|
248
|
+
stacklevel=2,
|
|
249
|
+
)
|
|
250
|
+
deprecated_settings[name] = arg
|
|
251
|
+
|
|
252
|
+
combined_settings = fastmcp.settings.model_dump() | deprecated_settings
|
|
253
|
+
self._deprecated_settings = Settings(**combined_settings)
|
|
254
|
+
|
|
201
255
|
@property
|
|
202
256
|
def name(self) -> str:
|
|
203
257
|
return self._mcp_server.name
|
|
@@ -244,12 +298,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
244
298
|
def _setup_handlers(self) -> None:
|
|
245
299
|
"""Set up core MCP protocol handlers."""
|
|
246
300
|
self._mcp_server.list_tools()(self._mcp_list_tools)
|
|
247
|
-
self._mcp_server.call_tool()(self._mcp_call_tool)
|
|
248
301
|
self._mcp_server.list_resources()(self._mcp_list_resources)
|
|
249
|
-
self._mcp_server.
|
|
302
|
+
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
250
303
|
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
|
304
|
+
self._mcp_server.call_tool()(self._mcp_call_tool)
|
|
305
|
+
self._mcp_server.read_resource()(self._mcp_read_resource)
|
|
251
306
|
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
252
|
-
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
253
307
|
|
|
254
308
|
async def get_tools(self) -> dict[str, Tool]:
|
|
255
309
|
"""Get all registered tools, indexed by registered key."""
|
|
@@ -268,6 +322,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
268
322
|
self._cache.set("tools", tools)
|
|
269
323
|
return tools
|
|
270
324
|
|
|
325
|
+
async def get_tool(self, key: str) -> Tool:
|
|
326
|
+
tools = await self.get_tools()
|
|
327
|
+
if key not in tools:
|
|
328
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
329
|
+
return tools[key]
|
|
330
|
+
|
|
271
331
|
async def get_resources(self) -> dict[str, Resource]:
|
|
272
332
|
"""Get all registered resources, indexed by registered key."""
|
|
273
333
|
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
|
@@ -285,6 +345,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
285
345
|
self._cache.set("resources", resources)
|
|
286
346
|
return resources
|
|
287
347
|
|
|
348
|
+
async def get_resource(self, key: str) -> Resource:
|
|
349
|
+
resources = await self.get_resources()
|
|
350
|
+
if key not in resources:
|
|
351
|
+
raise NotFoundError(f"Unknown resource: {key}")
|
|
352
|
+
return resources[key]
|
|
353
|
+
|
|
288
354
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
289
355
|
"""Get all registered resource templates, indexed by registered key."""
|
|
290
356
|
if (
|
|
@@ -305,6 +371,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
305
371
|
self._cache.set("resource_templates", templates)
|
|
306
372
|
return templates
|
|
307
373
|
|
|
374
|
+
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
375
|
+
templates = await self.get_resource_templates()
|
|
376
|
+
if key not in templates:
|
|
377
|
+
raise NotFoundError(f"Unknown resource template: {key}")
|
|
378
|
+
return templates[key]
|
|
379
|
+
|
|
308
380
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
309
381
|
"""
|
|
310
382
|
List all available prompts.
|
|
@@ -324,6 +396,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
324
396
|
self._cache.set("prompts", prompts)
|
|
325
397
|
return prompts
|
|
326
398
|
|
|
399
|
+
async def get_prompt(self, key: str) -> Prompt:
|
|
400
|
+
prompts = await self.get_prompts()
|
|
401
|
+
if key not in prompts:
|
|
402
|
+
raise NotFoundError(f"Unknown prompt: {key}")
|
|
403
|
+
return prompts[key]
|
|
404
|
+
|
|
327
405
|
def custom_route(
|
|
328
406
|
self,
|
|
329
407
|
path: str,
|
|
@@ -375,7 +453,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
375
453
|
|
|
376
454
|
"""
|
|
377
455
|
tools = await self.get_tools()
|
|
378
|
-
|
|
456
|
+
|
|
457
|
+
mcp_tools: list[MCPTool] = []
|
|
458
|
+
for key, tool in tools.items():
|
|
459
|
+
if self._should_enable_component(tool):
|
|
460
|
+
mcp_tools.append(tool.to_mcp_tool(name=key))
|
|
461
|
+
|
|
462
|
+
return mcp_tools
|
|
379
463
|
|
|
380
464
|
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
381
465
|
"""
|
|
@@ -384,9 +468,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
384
468
|
|
|
385
469
|
"""
|
|
386
470
|
resources = await self.get_resources()
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
471
|
+
mcp_resources: list[MCPResource] = []
|
|
472
|
+
for key, resource in resources.items():
|
|
473
|
+
if self._should_enable_component(resource):
|
|
474
|
+
mcp_resources.append(resource.to_mcp_resource(uri=key))
|
|
475
|
+
return mcp_resources
|
|
390
476
|
|
|
391
477
|
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
392
478
|
"""
|
|
@@ -395,10 +481,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
395
481
|
|
|
396
482
|
"""
|
|
397
483
|
templates = await self.get_resource_templates()
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
484
|
+
mcp_templates: list[MCPResourceTemplate] = []
|
|
485
|
+
for key, template in templates.items():
|
|
486
|
+
if self._should_enable_component(template):
|
|
487
|
+
mcp_templates.append(template.to_mcp_template(uriTemplate=key))
|
|
488
|
+
return mcp_templates
|
|
402
489
|
|
|
403
490
|
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
404
491
|
"""
|
|
@@ -407,12 +494,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
407
494
|
|
|
408
495
|
"""
|
|
409
496
|
prompts = await self.get_prompts()
|
|
410
|
-
|
|
497
|
+
mcp_prompts: list[MCPPrompt] = []
|
|
498
|
+
for key, prompt in prompts.items():
|
|
499
|
+
if self._should_enable_component(prompt):
|
|
500
|
+
mcp_prompts.append(prompt.to_mcp_prompt(name=key))
|
|
501
|
+
return mcp_prompts
|
|
411
502
|
|
|
412
503
|
async def _mcp_call_tool(
|
|
413
504
|
self, key: str, arguments: dict[str, Any]
|
|
414
505
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
415
|
-
"""
|
|
506
|
+
"""
|
|
507
|
+
Handle MCP 'callTool' requests.
|
|
508
|
+
|
|
509
|
+
Delegates to _call_tool, which should be overridden by FastMCP subclasses.
|
|
416
510
|
|
|
417
511
|
Args:
|
|
418
512
|
key: The name of the tool to call
|
|
@@ -425,43 +519,109 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
425
519
|
|
|
426
520
|
# Create and use context for the entire call
|
|
427
521
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
522
|
+
try:
|
|
523
|
+
return await self._call_tool(key, arguments)
|
|
524
|
+
except DisabledError:
|
|
525
|
+
# convert to NotFoundError to avoid leaking tool presence
|
|
526
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
527
|
+
except NotFoundError:
|
|
528
|
+
# standardize NotFound message
|
|
529
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
530
|
+
|
|
531
|
+
async def _call_tool(
|
|
532
|
+
self, key: str, arguments: dict[str, Any]
|
|
533
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
534
|
+
"""
|
|
535
|
+
Call a tool with raw MCP arguments. FastMCP subclasses should override
|
|
536
|
+
this method, not _mcp_call_tool.
|
|
431
537
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
tool_key = server.strip_tool_prefix(key)
|
|
436
|
-
return await server.server._mcp_call_tool(tool_key, arguments)
|
|
538
|
+
Args:
|
|
539
|
+
key: The name of the tool to call arguments: Arguments to pass to
|
|
540
|
+
the tool
|
|
437
541
|
|
|
438
|
-
|
|
542
|
+
Returns:
|
|
543
|
+
List of MCP Content objects containing the tool results
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
# Get tool, checking first from our tools, then from the mounted servers
|
|
547
|
+
if self._tool_manager.has_tool(key):
|
|
548
|
+
tool = self._tool_manager.get_tool(key)
|
|
549
|
+
if not self._should_enable_component(tool):
|
|
550
|
+
raise DisabledError(f"Tool {key!r} is disabled")
|
|
551
|
+
return await self._tool_manager.call_tool(key, arguments)
|
|
552
|
+
|
|
553
|
+
# Check mounted servers to see if they have the tool
|
|
554
|
+
for server in self._mounted_servers.values():
|
|
555
|
+
if server.match_tool(key):
|
|
556
|
+
tool_key = server.strip_tool_prefix(key)
|
|
557
|
+
return await server.server._call_tool(tool_key, arguments)
|
|
558
|
+
|
|
559
|
+
raise NotFoundError(f"Unknown tool: {key!r}")
|
|
439
560
|
|
|
440
561
|
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
562
|
+
"""
|
|
563
|
+
Handle MCP 'readResource' requests.
|
|
564
|
+
|
|
565
|
+
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
|
|
566
|
+
"""
|
|
567
|
+
logger.debug("Read resource: %s", uri)
|
|
568
|
+
|
|
569
|
+
with fastmcp.server.context.Context(fastmcp=self):
|
|
570
|
+
try:
|
|
571
|
+
return await self._read_resource(uri)
|
|
572
|
+
except DisabledError:
|
|
573
|
+
# convert to NotFoundError to avoid leaking resource presence
|
|
574
|
+
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
575
|
+
except NotFoundError:
|
|
576
|
+
# standardize NotFound message
|
|
577
|
+
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
578
|
+
|
|
579
|
+
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
441
580
|
"""
|
|
442
581
|
Read a resource by URI, in the format expected by the low-level MCP
|
|
443
582
|
server.
|
|
444
583
|
"""
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
584
|
+
if self._resource_manager.has_resource(uri):
|
|
585
|
+
resource = await self._resource_manager.get_resource(uri)
|
|
586
|
+
if not self._should_enable_component(resource):
|
|
587
|
+
raise DisabledError(f"Resource {str(uri)!r} is disabled")
|
|
588
|
+
content = await self._resource_manager.read_resource(uri)
|
|
589
|
+
return [
|
|
590
|
+
ReadResourceContents(
|
|
591
|
+
content=content,
|
|
592
|
+
mime_type=resource.mime_type,
|
|
593
|
+
)
|
|
594
|
+
]
|
|
595
|
+
else:
|
|
596
|
+
for server in self._mounted_servers.values():
|
|
597
|
+
if server.match_resource(str(uri)):
|
|
598
|
+
new_uri = server.strip_resource_prefix(str(uri))
|
|
599
|
+
return await server.server._mcp_read_resource(new_uri)
|
|
455
600
|
else:
|
|
456
|
-
|
|
457
|
-
if server.match_resource(str(uri)):
|
|
458
|
-
new_uri = server.strip_resource_prefix(str(uri))
|
|
459
|
-
return await server.server._mcp_read_resource(new_uri)
|
|
460
|
-
else:
|
|
461
|
-
raise NotFoundError(f"Unknown resource: {uri}")
|
|
601
|
+
raise NotFoundError(f"Unknown resource: {uri}")
|
|
462
602
|
|
|
463
603
|
async def _mcp_get_prompt(
|
|
464
604
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
605
|
+
) -> GetPromptResult:
|
|
606
|
+
"""
|
|
607
|
+
Handle MCP 'getPrompt' requests.
|
|
608
|
+
|
|
609
|
+
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
|
|
610
|
+
"""
|
|
611
|
+
logger.debug("Get prompt: %s with %s", name, arguments)
|
|
612
|
+
|
|
613
|
+
with fastmcp.server.context.Context(fastmcp=self):
|
|
614
|
+
try:
|
|
615
|
+
return await self._get_prompt(name, arguments)
|
|
616
|
+
except DisabledError:
|
|
617
|
+
# convert to NotFoundError to avoid leaking prompt presence
|
|
618
|
+
raise NotFoundError(f"Unknown prompt: {name}")
|
|
619
|
+
except NotFoundError:
|
|
620
|
+
# standardize NotFound message
|
|
621
|
+
raise NotFoundError(f"Unknown prompt: {name}")
|
|
622
|
+
|
|
623
|
+
async def _get_prompt(
|
|
624
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
465
625
|
) -> GetPromptResult:
|
|
466
626
|
"""Handle MCP 'getPrompt' requests.
|
|
467
627
|
|
|
@@ -474,19 +634,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
474
634
|
"""
|
|
475
635
|
logger.debug("Get prompt: %s with %s", name, arguments)
|
|
476
636
|
|
|
477
|
-
#
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if self.
|
|
481
|
-
|
|
637
|
+
# Get prompt, checking first from our prompts, then from the mounted servers
|
|
638
|
+
if self._prompt_manager.has_prompt(name):
|
|
639
|
+
prompt = self._prompt_manager.get_prompt(name)
|
|
640
|
+
if not self._should_enable_component(prompt):
|
|
641
|
+
raise DisabledError(f"Prompt {name!r} is disabled")
|
|
642
|
+
return await self._prompt_manager.render_prompt(name, arguments)
|
|
482
643
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
644
|
+
# Check mounted servers to see if they have the prompt
|
|
645
|
+
for server in self._mounted_servers.values():
|
|
646
|
+
if server.match_prompt(name):
|
|
647
|
+
prompt_name = server.strip_prompt_prefix(name)
|
|
648
|
+
return await server.server._mcp_get_prompt(prompt_name, arguments)
|
|
488
649
|
|
|
489
|
-
|
|
650
|
+
raise NotFoundError(f"Unknown prompt: {name}")
|
|
490
651
|
|
|
491
652
|
def add_tool(self, tool: Tool) -> None:
|
|
492
653
|
"""Add a tool to the server.
|
|
@@ -522,6 +683,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
522
683
|
tags: set[str] | None = None,
|
|
523
684
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
524
685
|
exclude_args: list[str] | None = None,
|
|
686
|
+
enabled: bool | None = None,
|
|
525
687
|
) -> FunctionTool: ...
|
|
526
688
|
|
|
527
689
|
@overload
|
|
@@ -534,6 +696,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
534
696
|
tags: set[str] | None = None,
|
|
535
697
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
536
698
|
exclude_args: list[str] | None = None,
|
|
699
|
+
enabled: bool | None = None,
|
|
537
700
|
) -> Callable[[AnyFunction], FunctionTool]: ...
|
|
538
701
|
|
|
539
702
|
def tool(
|
|
@@ -545,6 +708,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
545
708
|
tags: set[str] | None = None,
|
|
546
709
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
547
710
|
exclude_args: list[str] | None = None,
|
|
711
|
+
enabled: bool | None = None,
|
|
548
712
|
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
|
549
713
|
"""Decorator to register a tool.
|
|
550
714
|
|
|
@@ -561,11 +725,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
561
725
|
|
|
562
726
|
Args:
|
|
563
727
|
name_or_fn: Either a function (when used as @tool), a string name, or None
|
|
728
|
+
name: Optional name for the tool (keyword-only, alternative to name_or_fn)
|
|
564
729
|
description: Optional description of what the tool does
|
|
565
730
|
tags: Optional set of tags for categorizing the tool
|
|
566
|
-
annotations: Optional annotations about the tool's behavior
|
|
731
|
+
annotations: Optional annotations about the tool's behavior (e.g. {"is_async": True})
|
|
567
732
|
exclude_args: Optional list of argument names to exclude from the tool schema
|
|
568
|
-
|
|
733
|
+
enabled: Optional boolean to enable or disable the tool
|
|
569
734
|
|
|
570
735
|
Example:
|
|
571
736
|
@server.tool
|
|
@@ -618,6 +783,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
618
783
|
annotations=annotations,
|
|
619
784
|
exclude_args=exclude_args,
|
|
620
785
|
serializer=self._tool_serializer,
|
|
786
|
+
enabled=enabled,
|
|
621
787
|
)
|
|
622
788
|
self.add_tool(tool)
|
|
623
789
|
return tool
|
|
@@ -646,6 +812,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
646
812
|
tags=tags,
|
|
647
813
|
annotations=annotations,
|
|
648
814
|
exclude_args=exclude_args,
|
|
815
|
+
enabled=enabled,
|
|
649
816
|
)
|
|
650
817
|
|
|
651
818
|
def add_resource(self, resource: Resource, key: str | None = None) -> None:
|
|
@@ -712,6 +879,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
712
879
|
description: str | None = None,
|
|
713
880
|
mime_type: str | None = None,
|
|
714
881
|
tags: set[str] | None = None,
|
|
882
|
+
enabled: bool | None = None,
|
|
715
883
|
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
|
|
716
884
|
"""Decorator to register a function as a resource.
|
|
717
885
|
|
|
@@ -734,6 +902,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
734
902
|
description: Optional description of the resource
|
|
735
903
|
mime_type: Optional MIME type for the resource
|
|
736
904
|
tags: Optional set of tags for categorizing the resource
|
|
905
|
+
enabled: Optional boolean to enable or disable the resource
|
|
737
906
|
|
|
738
907
|
Example:
|
|
739
908
|
@server.resource("resource://my-resource")
|
|
@@ -798,6 +967,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
798
967
|
description=description,
|
|
799
968
|
mime_type=mime_type,
|
|
800
969
|
tags=tags,
|
|
970
|
+
enabled=enabled,
|
|
801
971
|
)
|
|
802
972
|
self.add_template(template)
|
|
803
973
|
return template
|
|
@@ -809,6 +979,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
809
979
|
description=description,
|
|
810
980
|
mime_type=mime_type,
|
|
811
981
|
tags=tags,
|
|
982
|
+
enabled=enabled,
|
|
812
983
|
)
|
|
813
984
|
self.add_resource(resource)
|
|
814
985
|
return resource
|
|
@@ -837,6 +1008,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
837
1008
|
name: str | None = None,
|
|
838
1009
|
description: str | None = None,
|
|
839
1010
|
tags: set[str] | None = None,
|
|
1011
|
+
enabled: bool | None = None,
|
|
840
1012
|
) -> FunctionPrompt: ...
|
|
841
1013
|
|
|
842
1014
|
@overload
|
|
@@ -847,6 +1019,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
847
1019
|
name: str | None = None,
|
|
848
1020
|
description: str | None = None,
|
|
849
1021
|
tags: set[str] | None = None,
|
|
1022
|
+
enabled: bool | None = None,
|
|
850
1023
|
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
|
851
1024
|
|
|
852
1025
|
def prompt(
|
|
@@ -856,6 +1029,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
856
1029
|
name: str | None = None,
|
|
857
1030
|
description: str | None = None,
|
|
858
1031
|
tags: set[str] | None = None,
|
|
1032
|
+
enabled: bool | None = None,
|
|
859
1033
|
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
|
|
860
1034
|
"""Decorator to register a prompt.
|
|
861
1035
|
|
|
@@ -872,9 +1046,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
872
1046
|
|
|
873
1047
|
Args:
|
|
874
1048
|
name_or_fn: Either a function (when used as @prompt), a string name, or None
|
|
1049
|
+
name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
|
|
875
1050
|
description: Optional description of what the prompt does
|
|
876
1051
|
tags: Optional set of tags for categorizing the prompt
|
|
877
|
-
|
|
1052
|
+
enabled: Optional boolean to enable or disable the prompt
|
|
878
1053
|
|
|
879
1054
|
Example:
|
|
880
1055
|
@server.prompt
|
|
@@ -947,6 +1122,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
947
1122
|
name=prompt_name,
|
|
948
1123
|
description=description,
|
|
949
1124
|
tags=tags,
|
|
1125
|
+
enabled=enabled,
|
|
950
1126
|
)
|
|
951
1127
|
self.add_prompt(prompt)
|
|
952
1128
|
|
|
@@ -974,6 +1150,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
974
1150
|
name=prompt_name,
|
|
975
1151
|
description=description,
|
|
976
1152
|
tags=tags,
|
|
1153
|
+
enabled=enabled,
|
|
977
1154
|
)
|
|
978
1155
|
|
|
979
1156
|
async def run_stdio_async(self) -> None:
|
|
@@ -1008,9 +1185,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1008
1185
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
1009
1186
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
1010
1187
|
"""
|
|
1011
|
-
host = host or self.
|
|
1012
|
-
port = port or self.
|
|
1013
|
-
default_log_level_to_use = (
|
|
1188
|
+
host = host or self._deprecated_settings.host
|
|
1189
|
+
port = port or self._deprecated_settings.port
|
|
1190
|
+
default_log_level_to_use = (
|
|
1191
|
+
log_level or self._deprecated_settings.log_level
|
|
1192
|
+
).lower()
|
|
1014
1193
|
|
|
1015
1194
|
app = self.http_app(path=path, transport=transport, middleware=middleware)
|
|
1016
1195
|
|
|
@@ -1084,10 +1263,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1084
1263
|
)
|
|
1085
1264
|
return create_sse_app(
|
|
1086
1265
|
server=self,
|
|
1087
|
-
message_path=message_path or self.
|
|
1088
|
-
sse_path=path or self.
|
|
1266
|
+
message_path=message_path or self._deprecated_settings.message_path,
|
|
1267
|
+
sse_path=path or self._deprecated_settings.sse_path,
|
|
1089
1268
|
auth=self.auth,
|
|
1090
|
-
debug=self.
|
|
1269
|
+
debug=self._deprecated_settings.debug,
|
|
1091
1270
|
middleware=middleware,
|
|
1092
1271
|
)
|
|
1093
1272
|
|
|
@@ -1115,6 +1294,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1115
1294
|
self,
|
|
1116
1295
|
path: str | None = None,
|
|
1117
1296
|
middleware: list[Middleware] | None = None,
|
|
1297
|
+
json_response: bool | None = None,
|
|
1298
|
+
stateless_http: bool | None = None,
|
|
1118
1299
|
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
|
1119
1300
|
) -> StarletteWithLifespan:
|
|
1120
1301
|
"""Create a Starlette app using the specified HTTP transport.
|
|
@@ -1131,21 +1312,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1131
1312
|
if transport == "streamable-http":
|
|
1132
1313
|
return create_streamable_http_app(
|
|
1133
1314
|
server=self,
|
|
1134
|
-
streamable_http_path=path
|
|
1315
|
+
streamable_http_path=path
|
|
1316
|
+
or self._deprecated_settings.streamable_http_path,
|
|
1135
1317
|
event_store=None,
|
|
1136
1318
|
auth=self.auth,
|
|
1137
|
-
json_response=self.
|
|
1138
|
-
stateless_http=self.
|
|
1139
|
-
debug=self.
|
|
1319
|
+
json_response=self._deprecated_settings.json_response,
|
|
1320
|
+
stateless_http=self._deprecated_settings.stateless_http,
|
|
1321
|
+
debug=self._deprecated_settings.debug,
|
|
1140
1322
|
middleware=middleware,
|
|
1141
1323
|
)
|
|
1142
1324
|
elif transport == "sse":
|
|
1143
1325
|
return create_sse_app(
|
|
1144
1326
|
server=self,
|
|
1145
|
-
message_path=self.
|
|
1146
|
-
sse_path=path or self.
|
|
1327
|
+
message_path=self._deprecated_settings.message_path,
|
|
1328
|
+
sse_path=path or self._deprecated_settings.sse_path,
|
|
1147
1329
|
auth=self.auth,
|
|
1148
|
-
debug=self.
|
|
1330
|
+
debug=self._deprecated_settings.debug,
|
|
1149
1331
|
middleware=middleware,
|
|
1150
1332
|
)
|
|
1151
1333
|
|
|
@@ -1376,28 +1558,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1376
1558
|
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
1377
1559
|
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
1378
1560
|
mcp_names: dict[str, str] | None = None,
|
|
1379
|
-
|
|
1561
|
+
tags: set[str] | None = None,
|
|
1380
1562
|
**settings: Any,
|
|
1381
1563
|
) -> FastMCPOpenAPI:
|
|
1382
1564
|
"""
|
|
1383
1565
|
Create a FastMCP server from an OpenAPI specification.
|
|
1384
1566
|
"""
|
|
1385
|
-
from .openapi import FastMCPOpenAPI
|
|
1386
|
-
|
|
1387
|
-
# Deprecated since 2.5.0
|
|
1388
|
-
if all_routes_as_tools:
|
|
1389
|
-
warnings.warn(
|
|
1390
|
-
"The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
|
|
1391
|
-
'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
|
|
1392
|
-
DeprecationWarning,
|
|
1393
|
-
stacklevel=2,
|
|
1394
|
-
)
|
|
1395
|
-
|
|
1396
|
-
if all_routes_as_tools and route_maps:
|
|
1397
|
-
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
|
1398
|
-
|
|
1399
|
-
elif all_routes_as_tools:
|
|
1400
|
-
route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
|
|
1567
|
+
from .openapi import FastMCPOpenAPI
|
|
1401
1568
|
|
|
1402
1569
|
return FastMCPOpenAPI(
|
|
1403
1570
|
openapi_spec=openapi_spec,
|
|
@@ -1406,6 +1573,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1406
1573
|
route_map_fn=route_map_fn,
|
|
1407
1574
|
mcp_component_fn=mcp_component_fn,
|
|
1408
1575
|
mcp_names=mcp_names,
|
|
1576
|
+
tags=tags,
|
|
1409
1577
|
**settings,
|
|
1410
1578
|
)
|
|
1411
1579
|
|
|
@@ -1418,30 +1586,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1418
1586
|
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
1419
1587
|
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
1420
1588
|
mcp_names: dict[str, str] | None = None,
|
|
1421
|
-
all_routes_as_tools: bool = False,
|
|
1422
1589
|
httpx_client_kwargs: dict[str, Any] | None = None,
|
|
1590
|
+
tags: set[str] | None = None,
|
|
1423
1591
|
**settings: Any,
|
|
1424
1592
|
) -> FastMCPOpenAPI:
|
|
1425
1593
|
"""
|
|
1426
1594
|
Create a FastMCP server from a FastAPI application.
|
|
1427
1595
|
"""
|
|
1428
1596
|
|
|
1429
|
-
from .openapi import FastMCPOpenAPI
|
|
1430
|
-
|
|
1431
|
-
# Deprecated since 2.5.0
|
|
1432
|
-
if all_routes_as_tools:
|
|
1433
|
-
warnings.warn(
|
|
1434
|
-
"The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
|
|
1435
|
-
'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
|
|
1436
|
-
DeprecationWarning,
|
|
1437
|
-
stacklevel=2,
|
|
1438
|
-
)
|
|
1439
|
-
|
|
1440
|
-
if all_routes_as_tools and route_maps:
|
|
1441
|
-
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
|
1442
|
-
|
|
1443
|
-
elif all_routes_as_tools:
|
|
1444
|
-
route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
|
|
1597
|
+
from .openapi import FastMCPOpenAPI
|
|
1445
1598
|
|
|
1446
1599
|
if httpx_client_kwargs is None:
|
|
1447
1600
|
httpx_client_kwargs = {}
|
|
@@ -1462,6 +1615,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1462
1615
|
route_map_fn=route_map_fn,
|
|
1463
1616
|
mcp_component_fn=mcp_component_fn,
|
|
1464
1617
|
mcp_names=mcp_names,
|
|
1618
|
+
tags=tags,
|
|
1465
1619
|
**settings,
|
|
1466
1620
|
)
|
|
1467
1621
|
|
|
@@ -1511,6 +1665,41 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1511
1665
|
|
|
1512
1666
|
return cls.as_proxy(client, **settings)
|
|
1513
1667
|
|
|
1668
|
+
def _should_enable_component(
|
|
1669
|
+
self,
|
|
1670
|
+
component: FastMCPComponent,
|
|
1671
|
+
) -> bool:
|
|
1672
|
+
"""
|
|
1673
|
+
Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
|
|
1674
|
+
|
|
1675
|
+
Rules:
|
|
1676
|
+
• If the component's enabled property is False, always return False.
|
|
1677
|
+
• If both include_tags and exclude_tags are None, return True.
|
|
1678
|
+
• If exclude_tags is provided, check each exclude tag:
|
|
1679
|
+
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
|
1680
|
+
• If include_tags is provided, check each include tag:
|
|
1681
|
+
- If the include tag is a string, it must be present in the input tags to include.
|
|
1682
|
+
• If include_tags is provided and none of the include tags match, return False.
|
|
1683
|
+
• If include_tags is not provided, return True.
|
|
1684
|
+
"""
|
|
1685
|
+
if not component.enabled:
|
|
1686
|
+
return False
|
|
1687
|
+
|
|
1688
|
+
if self.include_tags is None and self.exclude_tags is None:
|
|
1689
|
+
return True
|
|
1690
|
+
|
|
1691
|
+
if self.exclude_tags is not None:
|
|
1692
|
+
if any(etag in component.tags for etag in self.exclude_tags):
|
|
1693
|
+
return False
|
|
1694
|
+
|
|
1695
|
+
if self.include_tags is not None:
|
|
1696
|
+
if any(itag in component.tags for itag in self.include_tags):
|
|
1697
|
+
return True
|
|
1698
|
+
else:
|
|
1699
|
+
return False
|
|
1700
|
+
|
|
1701
|
+
return True
|
|
1702
|
+
|
|
1514
1703
|
|
|
1515
1704
|
class MountedServer:
|
|
1516
1705
|
def __init__(
|
|
@@ -1597,7 +1786,7 @@ def add_resource_prefix(
|
|
|
1597
1786
|
# Get the server settings to check for legacy format preference
|
|
1598
1787
|
|
|
1599
1788
|
if prefix_format is None:
|
|
1600
|
-
prefix_format = fastmcp.settings.
|
|
1789
|
+
prefix_format = fastmcp.settings.resource_prefix_format
|
|
1601
1790
|
|
|
1602
1791
|
if prefix_format == "protocol":
|
|
1603
1792
|
# Legacy style: prefix+protocol://path
|
|
@@ -1646,7 +1835,7 @@ def remove_resource_prefix(
|
|
|
1646
1835
|
return uri
|
|
1647
1836
|
|
|
1648
1837
|
if prefix_format is None:
|
|
1649
|
-
prefix_format = fastmcp.settings.
|
|
1838
|
+
prefix_format = fastmcp.settings.resource_prefix_format
|
|
1650
1839
|
|
|
1651
1840
|
if prefix_format == "protocol":
|
|
1652
1841
|
# Legacy style: prefix+protocol://path
|
|
@@ -1706,7 +1895,7 @@ def has_resource_prefix(
|
|
|
1706
1895
|
# Get the server settings to check for legacy format preference
|
|
1707
1896
|
|
|
1708
1897
|
if prefix_format is None:
|
|
1709
|
-
prefix_format = fastmcp.settings.
|
|
1898
|
+
prefix_format = fastmcp.settings.resource_prefix_format
|
|
1710
1899
|
|
|
1711
1900
|
if prefix_format == "protocol":
|
|
1712
1901
|
# Legacy style: prefix+protocol://path
|