fastmcp 2.8.0__py3-none-any.whl → 2.8.1__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 +28 -2
- fastmcp/client/client.py +3 -4
- fastmcp/client/sampling.py +5 -9
- fastmcp/client/transports.py +7 -5
- fastmcp/prompts/prompt.py +3 -3
- fastmcp/prompts/prompt_manager.py +6 -5
- fastmcp/resources/resource_manager.py +12 -10
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/context.py +12 -10
- fastmcp/server/openapi.py +22 -19
- fastmcp/server/proxy.py +3 -7
- fastmcp/server/server.py +112 -85
- fastmcp/settings.py +20 -7
- fastmcp/tools/tool.py +11 -10
- fastmcp/tools/tool_manager.py +9 -9
- fastmcp/tools/tool_transform.py +9 -5
- fastmcp/utilities/types.py +82 -5
- {fastmcp-2.8.0.dist-info → fastmcp-2.8.1.dist-info}/METADATA +46 -26
- {fastmcp-2.8.0.dist-info → fastmcp-2.8.1.dist-info}/RECORD +22 -22
- {fastmcp-2.8.0.dist-info → fastmcp-2.8.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.8.0.dist-info → fastmcp-2.8.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.8.0.dist-info → fastmcp-2.8.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -25,10 +25,7 @@ from mcp.server.lowlevel.server import Server as MCPServer
|
|
|
25
25
|
from mcp.server.stdio import stdio_server
|
|
26
26
|
from mcp.types import (
|
|
27
27
|
AnyFunction,
|
|
28
|
-
EmbeddedResource,
|
|
29
28
|
GetPromptResult,
|
|
30
|
-
ImageContent,
|
|
31
|
-
TextContent,
|
|
32
29
|
ToolAnnotations,
|
|
33
30
|
)
|
|
34
31
|
from mcp.types import Prompt as MCPPrompt
|
|
@@ -62,6 +59,7 @@ from fastmcp.utilities.cache import TimedCache
|
|
|
62
59
|
from fastmcp.utilities.components import FastMCPComponent
|
|
63
60
|
from fastmcp.utilities.logging import get_logger
|
|
64
61
|
from fastmcp.utilities.mcp_config import MCPConfig
|
|
62
|
+
from fastmcp.utilities.types import MCPContent
|
|
65
63
|
|
|
66
64
|
if TYPE_CHECKING:
|
|
67
65
|
from fastmcp.client import Client
|
|
@@ -242,16 +240,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
242
240
|
]:
|
|
243
241
|
if arg is not None:
|
|
244
242
|
# Deprecated in 2.8.0
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
if fastmcp.settings.deprecation_warnings:
|
|
244
|
+
warnings.warn(
|
|
245
|
+
f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.",
|
|
246
|
+
DeprecationWarning,
|
|
247
|
+
stacklevel=2,
|
|
248
|
+
)
|
|
250
249
|
deprecated_settings[name] = arg
|
|
251
250
|
|
|
252
251
|
combined_settings = fastmcp.settings.model_dump() | deprecated_settings
|
|
253
252
|
self._deprecated_settings = Settings(**combined_settings)
|
|
254
253
|
|
|
254
|
+
@property
|
|
255
|
+
def settings(self) -> Settings:
|
|
256
|
+
# Deprecated in 2.8.0
|
|
257
|
+
if fastmcp.settings.deprecation_warnings:
|
|
258
|
+
warnings.warn(
|
|
259
|
+
"Accessing `.settings` on a FastMCP instance is deprecated. Use the global `fastmcp.settings` instead.",
|
|
260
|
+
DeprecationWarning,
|
|
261
|
+
stacklevel=2,
|
|
262
|
+
)
|
|
263
|
+
return self._deprecated_settings
|
|
264
|
+
|
|
255
265
|
@property
|
|
256
266
|
def name(self) -> str:
|
|
257
267
|
return self._mcp_server.name
|
|
@@ -502,7 +512,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
502
512
|
|
|
503
513
|
async def _mcp_call_tool(
|
|
504
514
|
self, key: str, arguments: dict[str, Any]
|
|
505
|
-
) -> list[
|
|
515
|
+
) -> list[MCPContent]:
|
|
506
516
|
"""
|
|
507
517
|
Handle MCP 'callTool' requests.
|
|
508
518
|
|
|
@@ -528,9 +538,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
528
538
|
# standardize NotFound message
|
|
529
539
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
530
540
|
|
|
531
|
-
async def _call_tool(
|
|
532
|
-
self, key: str, arguments: dict[str, Any]
|
|
533
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
541
|
+
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
534
542
|
"""
|
|
535
543
|
Call a tool with raw MCP arguments. FastMCP subclasses should override
|
|
536
544
|
this method, not _mcp_call_tool.
|
|
@@ -856,11 +864,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
856
864
|
tags: Optional set of tags for categorizing the resource
|
|
857
865
|
"""
|
|
858
866
|
# deprecated since 2.7.0
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
867
|
+
if fastmcp.settings.deprecation_warnings:
|
|
868
|
+
warnings.warn(
|
|
869
|
+
"The add_resource_fn method is deprecated. Use the resource decorator instead.",
|
|
870
|
+
DeprecationWarning,
|
|
871
|
+
stacklevel=2,
|
|
872
|
+
)
|
|
864
873
|
self._resource_manager.add_resource_or_template_from_fn(
|
|
865
874
|
fn=fn,
|
|
866
875
|
uri=uri,
|
|
@@ -1218,19 +1227,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1218
1227
|
port: int | None = None,
|
|
1219
1228
|
log_level: str | None = None,
|
|
1220
1229
|
path: str | None = None,
|
|
1221
|
-
message_path: str | None = None,
|
|
1222
1230
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1223
1231
|
) -> None:
|
|
1224
1232
|
"""Run the server using SSE transport."""
|
|
1225
1233
|
|
|
1226
1234
|
# Deprecated since 2.3.2
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1235
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1236
|
+
warnings.warn(
|
|
1237
|
+
"The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
|
|
1238
|
+
"modern (non-SSE) alternative, or create an SSE app with "
|
|
1239
|
+
"`fastmcp.server.http.create_sse_app` and run it directly.",
|
|
1240
|
+
DeprecationWarning,
|
|
1241
|
+
stacklevel=2,
|
|
1242
|
+
)
|
|
1234
1243
|
await self.run_http_async(
|
|
1235
1244
|
transport="sse",
|
|
1236
1245
|
host=host,
|
|
@@ -1255,12 +1264,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1255
1264
|
middleware: A list of middleware to apply to the app
|
|
1256
1265
|
"""
|
|
1257
1266
|
# Deprecated since 2.3.2
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1267
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1268
|
+
warnings.warn(
|
|
1269
|
+
"The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
|
|
1270
|
+
"alternative, or call `fastmcp.server.http.create_sse_app` directly.",
|
|
1271
|
+
DeprecationWarning,
|
|
1272
|
+
stacklevel=2,
|
|
1273
|
+
)
|
|
1264
1274
|
return create_sse_app(
|
|
1265
1275
|
server=self,
|
|
1266
1276
|
message_path=message_path or self._deprecated_settings.message_path,
|
|
@@ -1283,11 +1293,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1283
1293
|
middleware: A list of middleware to apply to the app
|
|
1284
1294
|
"""
|
|
1285
1295
|
# Deprecated since 2.3.2
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1296
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1297
|
+
warnings.warn(
|
|
1298
|
+
"The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
|
|
1299
|
+
DeprecationWarning,
|
|
1300
|
+
stacklevel=2,
|
|
1301
|
+
)
|
|
1291
1302
|
return self.http_app(path=path, middleware=middleware)
|
|
1292
1303
|
|
|
1293
1304
|
def http_app(
|
|
@@ -1316,8 +1327,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1316
1327
|
or self._deprecated_settings.streamable_http_path,
|
|
1317
1328
|
event_store=None,
|
|
1318
1329
|
auth=self.auth,
|
|
1319
|
-
json_response=
|
|
1320
|
-
|
|
1330
|
+
json_response=(
|
|
1331
|
+
json_response
|
|
1332
|
+
if json_response is not None
|
|
1333
|
+
else self._deprecated_settings.json_response
|
|
1334
|
+
),
|
|
1335
|
+
stateless_http=(
|
|
1336
|
+
stateless_http
|
|
1337
|
+
if stateless_http is not None
|
|
1338
|
+
else self._deprecated_settings.stateless_http
|
|
1339
|
+
),
|
|
1321
1340
|
debug=self._deprecated_settings.debug,
|
|
1322
1341
|
middleware=middleware,
|
|
1323
1342
|
)
|
|
@@ -1340,12 +1359,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1340
1359
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1341
1360
|
) -> None:
|
|
1342
1361
|
# Deprecated since 2.3.2
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1362
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1363
|
+
warnings.warn(
|
|
1364
|
+
"The run_streamable_http_async method is deprecated (as of 2.3.2). "
|
|
1365
|
+
"Use run_http_async instead.",
|
|
1366
|
+
DeprecationWarning,
|
|
1367
|
+
stacklevel=2,
|
|
1368
|
+
)
|
|
1349
1369
|
await self.run_http_async(
|
|
1350
1370
|
transport="streamable-http",
|
|
1351
1371
|
host=host,
|
|
@@ -1413,30 +1433,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1413
1433
|
|
|
1414
1434
|
if tool_separator is not None:
|
|
1415
1435
|
# Deprecated since 2.4.0
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1436
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1437
|
+
warnings.warn(
|
|
1438
|
+
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1439
|
+
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1440
|
+
DeprecationWarning,
|
|
1441
|
+
stacklevel=2,
|
|
1442
|
+
)
|
|
1422
1443
|
|
|
1423
1444
|
if resource_separator is not None:
|
|
1424
1445
|
# Deprecated since 2.4.0
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1446
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1447
|
+
warnings.warn(
|
|
1448
|
+
"The resource_separator parameter is deprecated and ignored. "
|
|
1449
|
+
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1450
|
+
DeprecationWarning,
|
|
1451
|
+
stacklevel=2,
|
|
1452
|
+
)
|
|
1431
1453
|
|
|
1432
1454
|
if prompt_separator is not None:
|
|
1433
1455
|
# Deprecated since 2.4.0
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1456
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1457
|
+
warnings.warn(
|
|
1458
|
+
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1459
|
+
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1460
|
+
DeprecationWarning,
|
|
1461
|
+
stacklevel=2,
|
|
1462
|
+
)
|
|
1440
1463
|
|
|
1441
1464
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1442
1465
|
# we should treat it as a proxy
|
|
@@ -1498,30 +1521,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1498
1521
|
"""
|
|
1499
1522
|
if tool_separator is not None:
|
|
1500
1523
|
# Deprecated since 2.4.0
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1524
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1525
|
+
warnings.warn(
|
|
1526
|
+
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1527
|
+
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1528
|
+
DeprecationWarning,
|
|
1529
|
+
stacklevel=2,
|
|
1530
|
+
)
|
|
1507
1531
|
|
|
1508
1532
|
if resource_separator is not None:
|
|
1509
1533
|
# Deprecated since 2.4.0
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1534
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1535
|
+
warnings.warn(
|
|
1536
|
+
"The resource_separator parameter is deprecated and ignored. "
|
|
1537
|
+
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1538
|
+
DeprecationWarning,
|
|
1539
|
+
stacklevel=2,
|
|
1540
|
+
)
|
|
1516
1541
|
|
|
1517
1542
|
if prompt_separator is not None:
|
|
1518
1543
|
# Deprecated since 2.4.0
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1544
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1545
|
+
warnings.warn(
|
|
1546
|
+
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1547
|
+
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1548
|
+
DeprecationWarning,
|
|
1549
|
+
stacklevel=2,
|
|
1550
|
+
)
|
|
1525
1551
|
|
|
1526
1552
|
# Import tools from the mounted server
|
|
1527
1553
|
tool_prefix = f"{prefix}_"
|
|
@@ -1657,11 +1683,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1657
1683
|
Create a FastMCP proxy server from a FastMCP client.
|
|
1658
1684
|
"""
|
|
1659
1685
|
# Deprecated since 2.3.5
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1686
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1687
|
+
warnings.warn(
|
|
1688
|
+
"FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
|
|
1689
|
+
DeprecationWarning,
|
|
1690
|
+
stacklevel=2,
|
|
1691
|
+
)
|
|
1665
1692
|
|
|
1666
1693
|
return cls.as_proxy(client, **settings)
|
|
1667
1694
|
|
fastmcp/settings.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
import warnings
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from typing import Annotated, Any, Literal
|
|
7
6
|
|
|
@@ -15,6 +14,10 @@ from pydantic_settings import (
|
|
|
15
14
|
)
|
|
16
15
|
from typing_extensions import Self
|
|
17
16
|
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
18
21
|
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
19
22
|
|
|
20
23
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
@@ -39,10 +42,8 @@ class ExtendedEnvSettingsSource(EnvSettingsSource):
|
|
|
39
42
|
if env_val is not None:
|
|
40
43
|
if prefix == "FASTMCP_SERVER_":
|
|
41
44
|
# Deprecated in 2.8.0
|
|
42
|
-
|
|
45
|
+
logger.warning(
|
|
43
46
|
"Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.",
|
|
44
|
-
DeprecationWarning,
|
|
45
|
-
stacklevel=2,
|
|
46
47
|
)
|
|
47
48
|
return env_val, field_key, value_is_complex
|
|
48
49
|
|
|
@@ -89,10 +90,8 @@ class Settings(BaseSettings):
|
|
|
89
90
|
which accessed fastmcp.settings.settings
|
|
90
91
|
"""
|
|
91
92
|
# Deprecated in 2.8.0
|
|
92
|
-
|
|
93
|
+
logger.warning(
|
|
93
94
|
"Using fastmcp.settings.settings is deprecated. Use fastmcp.settings instead.",
|
|
94
|
-
DeprecationWarning,
|
|
95
|
-
stacklevel=2,
|
|
96
95
|
)
|
|
97
96
|
return self
|
|
98
97
|
|
|
@@ -111,6 +110,20 @@ class Settings(BaseSettings):
|
|
|
111
110
|
),
|
|
112
111
|
] = True
|
|
113
112
|
|
|
113
|
+
deprecation_warnings: Annotated[
|
|
114
|
+
bool,
|
|
115
|
+
Field(
|
|
116
|
+
description=inspect.cleandoc(
|
|
117
|
+
"""
|
|
118
|
+
Whether to show deprecation warnings. You can completely reset
|
|
119
|
+
Python's warning behavior by running `warnings.resetwarnings()`.
|
|
120
|
+
Note this will NOT apply to deprecation warnings from the
|
|
121
|
+
settings class itself.
|
|
122
|
+
""",
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
] = True
|
|
126
|
+
|
|
114
127
|
client_raise_first_exceptiongroup_error: Annotated[
|
|
115
128
|
bool,
|
|
116
129
|
Field(
|
fastmcp/tools/tool.py
CHANGED
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
import pydantic_core
|
|
10
|
-
from mcp.types import
|
|
10
|
+
from mcp.types import TextContent, ToolAnnotations
|
|
11
11
|
from mcp.types import Tool as MCPTool
|
|
12
12
|
from pydantic import Field
|
|
13
13
|
|
|
@@ -17,7 +17,9 @@ from fastmcp.utilities.components import FastMCPComponent
|
|
|
17
17
|
from fastmcp.utilities.json_schema import compress_schema
|
|
18
18
|
from fastmcp.utilities.logging import get_logger
|
|
19
19
|
from fastmcp.utilities.types import (
|
|
20
|
+
Audio,
|
|
20
21
|
Image,
|
|
22
|
+
MCPContent,
|
|
21
23
|
find_kwarg_by_type,
|
|
22
24
|
get_cached_typeadapter,
|
|
23
25
|
)
|
|
@@ -75,9 +77,7 @@ class Tool(FastMCPComponent):
|
|
|
75
77
|
enabled=enabled,
|
|
76
78
|
)
|
|
77
79
|
|
|
78
|
-
async def run(
|
|
79
|
-
self, arguments: dict[str, Any]
|
|
80
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
80
|
+
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
81
81
|
"""Run the tool with arguments."""
|
|
82
82
|
raise NotImplementedError("Subclasses must implement run()")
|
|
83
83
|
|
|
@@ -142,9 +142,7 @@ class FunctionTool(Tool):
|
|
|
142
142
|
enabled=enabled if enabled is not None else True,
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
async def run(
|
|
146
|
-
self, arguments: dict[str, Any]
|
|
147
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
145
|
+
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
148
146
|
"""Run the tool with arguments."""
|
|
149
147
|
from fastmcp.server.context import Context
|
|
150
148
|
|
|
@@ -265,17 +263,20 @@ def _convert_to_content(
|
|
|
265
263
|
result: Any,
|
|
266
264
|
serializer: Callable[[Any], str] | None = None,
|
|
267
265
|
_process_as_single_item: bool = False,
|
|
268
|
-
) -> list[
|
|
266
|
+
) -> list[MCPContent]:
|
|
269
267
|
"""Convert a result to a sequence of content objects."""
|
|
270
268
|
if result is None:
|
|
271
269
|
return []
|
|
272
270
|
|
|
273
|
-
if isinstance(result,
|
|
271
|
+
if isinstance(result, MCPContent):
|
|
274
272
|
return [result]
|
|
275
273
|
|
|
276
274
|
if isinstance(result, Image):
|
|
277
275
|
return [result.to_image_content()]
|
|
278
276
|
|
|
277
|
+
elif isinstance(result, Audio):
|
|
278
|
+
return [result.to_audio_content()]
|
|
279
|
+
|
|
279
280
|
if isinstance(result, list | tuple) and not _process_as_single_item:
|
|
280
281
|
# if the result is a list, then it could either be a list of MCP types,
|
|
281
282
|
# or a "regular" list that the tool is returning, or a mix of both.
|
|
@@ -287,7 +288,7 @@ def _convert_to_content(
|
|
|
287
288
|
other_content = []
|
|
288
289
|
|
|
289
290
|
for item in result:
|
|
290
|
-
if isinstance(item,
|
|
291
|
+
if isinstance(item, MCPContent | Image | Audio):
|
|
291
292
|
mcp_types.append(_convert_to_content(item)[0])
|
|
292
293
|
else:
|
|
293
294
|
other_content.append(item)
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -4,13 +4,14 @@ import warnings
|
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
-
from mcp.types import
|
|
7
|
+
from mcp.types import ToolAnnotations
|
|
8
8
|
|
|
9
9
|
from fastmcp import settings
|
|
10
10
|
from fastmcp.exceptions import NotFoundError, ToolError
|
|
11
11
|
from fastmcp.settings import DuplicateBehavior
|
|
12
12
|
from fastmcp.tools.tool import Tool
|
|
13
13
|
from fastmcp.utilities.logging import get_logger
|
|
14
|
+
from fastmcp.utilities.types import MCPContent
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
pass
|
|
@@ -71,11 +72,12 @@ class ToolManager:
|
|
|
71
72
|
) -> Tool:
|
|
72
73
|
"""Add a tool to the server."""
|
|
73
74
|
# deprecated in 2.7.0
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
if settings.deprecation_warnings:
|
|
76
|
+
warnings.warn(
|
|
77
|
+
"ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.",
|
|
78
|
+
DeprecationWarning,
|
|
79
|
+
stacklevel=2,
|
|
80
|
+
)
|
|
79
81
|
tool = Tool.from_function(
|
|
80
82
|
fn,
|
|
81
83
|
name=name,
|
|
@@ -119,9 +121,7 @@ class ToolManager:
|
|
|
119
121
|
else:
|
|
120
122
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
121
123
|
|
|
122
|
-
async def call_tool(
|
|
123
|
-
self, key: str, arguments: dict[str, Any]
|
|
124
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
124
|
+
async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
125
125
|
"""Call a tool by name with arguments."""
|
|
126
126
|
tool = self.get_tool(key)
|
|
127
127
|
if not tool:
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -7,12 +7,12 @@ from dataclasses import dataclass
|
|
|
7
7
|
from types import EllipsisType
|
|
8
8
|
from typing import Any, Literal
|
|
9
9
|
|
|
10
|
-
from mcp.types import
|
|
10
|
+
from mcp.types import ToolAnnotations
|
|
11
11
|
from pydantic import ConfigDict
|
|
12
12
|
|
|
13
13
|
from fastmcp.tools.tool import ParsedFunction, Tool
|
|
14
14
|
from fastmcp.utilities.logging import get_logger
|
|
15
|
-
from fastmcp.utilities.types import get_cached_typeadapter
|
|
15
|
+
from fastmcp.utilities.types import MCPContent, get_cached_typeadapter
|
|
16
16
|
|
|
17
17
|
logger = get_logger(__name__)
|
|
18
18
|
|
|
@@ -97,6 +97,7 @@ class ArgTransform:
|
|
|
97
97
|
type: New type for the argument. Use ... for no change.
|
|
98
98
|
hide: If True, hide this argument from clients but pass a constant value to parent.
|
|
99
99
|
required: If True, make argument required (remove default). Use ... for no change.
|
|
100
|
+
examples: Examples for the argument. Use ... for no change.
|
|
100
101
|
|
|
101
102
|
Examples:
|
|
102
103
|
# Rename argument 'old_name' to 'new_name'
|
|
@@ -137,6 +138,7 @@ class ArgTransform:
|
|
|
137
138
|
type: Any | EllipsisType = NotSet
|
|
138
139
|
hide: bool = False
|
|
139
140
|
required: Literal[True] | EllipsisType = NotSet
|
|
141
|
+
examples: Any | EllipsisType = NotSet
|
|
140
142
|
|
|
141
143
|
def __post_init__(self):
|
|
142
144
|
"""Validate that only one of default or default_factory is provided."""
|
|
@@ -200,9 +202,7 @@ class TransformedTool(Tool):
|
|
|
200
202
|
forwarding_fn: Callable[..., Any] # Always present, handles arg transformation
|
|
201
203
|
transform_args: dict[str, ArgTransform]
|
|
202
204
|
|
|
203
|
-
async def run(
|
|
204
|
-
self, arguments: dict[str, Any]
|
|
205
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
205
|
+
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
206
206
|
"""Run the tool with context set for forward() functions.
|
|
207
207
|
|
|
208
208
|
This method executes the tool's function while setting up the context
|
|
@@ -584,6 +584,10 @@ class TransformedTool(Tool):
|
|
|
584
584
|
# Update the schema with the type information from TypeAdapter
|
|
585
585
|
new_schema.update(type_schema)
|
|
586
586
|
|
|
587
|
+
# Handle examples transformation
|
|
588
|
+
if transform.examples is not NotSet:
|
|
589
|
+
new_schema["examples"] = transform.examples
|
|
590
|
+
|
|
587
591
|
return new_name, new_schema, is_required
|
|
588
592
|
|
|
589
593
|
@staticmethod
|
fastmcp/utilities/types.py
CHANGED
|
@@ -6,13 +6,21 @@ from collections.abc import Callable
|
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from types import UnionType
|
|
9
|
-
from typing import Annotated, TypeVar, Union, get_args, get_origin
|
|
10
|
-
|
|
11
|
-
from mcp.types import
|
|
9
|
+
from typing import Annotated, TypeAlias, TypeVar, Union, get_args, get_origin
|
|
10
|
+
|
|
11
|
+
from mcp.types import (
|
|
12
|
+
Annotations,
|
|
13
|
+
AudioContent,
|
|
14
|
+
EmbeddedResource,
|
|
15
|
+
ImageContent,
|
|
16
|
+
TextContent,
|
|
17
|
+
)
|
|
12
18
|
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
|
13
19
|
|
|
14
20
|
T = TypeVar("T")
|
|
15
21
|
|
|
22
|
+
MCPContent: TypeAlias = TextContent | ImageContent | AudioContent | EmbeddedResource
|
|
23
|
+
|
|
16
24
|
|
|
17
25
|
class FastMCPBaseModel(BaseModel):
|
|
18
26
|
"""Base model for FastMCP models."""
|
|
@@ -88,6 +96,7 @@ class Image:
|
|
|
88
96
|
path: str | Path | None = None,
|
|
89
97
|
data: bytes | None = None,
|
|
90
98
|
format: str | None = None,
|
|
99
|
+
annotations: Annotations | None = None,
|
|
91
100
|
):
|
|
92
101
|
if path is None and data is None:
|
|
93
102
|
raise ValueError("Either path or data must be provided")
|
|
@@ -98,6 +107,7 @@ class Image:
|
|
|
98
107
|
self.data = data
|
|
99
108
|
self._format = format
|
|
100
109
|
self._mime_type = self._get_mime_type()
|
|
110
|
+
self.annotations = annotations
|
|
101
111
|
|
|
102
112
|
def _get_mime_type(self) -> str:
|
|
103
113
|
"""Get MIME type from format or guess from file extension."""
|
|
@@ -115,7 +125,11 @@ class Image:
|
|
|
115
125
|
}.get(suffix, "application/octet-stream")
|
|
116
126
|
return "image/png" # default for raw binary data
|
|
117
127
|
|
|
118
|
-
def to_image_content(
|
|
128
|
+
def to_image_content(
|
|
129
|
+
self,
|
|
130
|
+
mime_type: str | None = None,
|
|
131
|
+
annotations: Annotations | None = None,
|
|
132
|
+
) -> ImageContent:
|
|
119
133
|
"""Convert to MCP ImageContent."""
|
|
120
134
|
if self.path:
|
|
121
135
|
with open(self.path, "rb") as f:
|
|
@@ -125,4 +139,67 @@ class Image:
|
|
|
125
139
|
else:
|
|
126
140
|
raise ValueError("No image data available")
|
|
127
141
|
|
|
128
|
-
return ImageContent(
|
|
142
|
+
return ImageContent(
|
|
143
|
+
type="image",
|
|
144
|
+
data=data,
|
|
145
|
+
mimeType=mime_type or self._mime_type,
|
|
146
|
+
annotations=annotations or self.annotations,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Audio:
|
|
151
|
+
"""Helper class for returning audio from tools."""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
path: str | Path | None = None,
|
|
156
|
+
data: bytes | None = None,
|
|
157
|
+
format: str | None = None,
|
|
158
|
+
annotations: Annotations | None = None,
|
|
159
|
+
):
|
|
160
|
+
if path is None and data is None:
|
|
161
|
+
raise ValueError("Either path or data must be provided")
|
|
162
|
+
if path is not None and data is not None:
|
|
163
|
+
raise ValueError("Only one of path or data can be provided")
|
|
164
|
+
|
|
165
|
+
self.path = Path(path) if path else None
|
|
166
|
+
self.data = data
|
|
167
|
+
self._format = format
|
|
168
|
+
self._mime_type = self._get_mime_type()
|
|
169
|
+
self.annotations = annotations
|
|
170
|
+
|
|
171
|
+
def _get_mime_type(self) -> str:
|
|
172
|
+
"""Get MIME type from format or guess from file extension."""
|
|
173
|
+
if self._format:
|
|
174
|
+
return f"audio/{self._format.lower()}"
|
|
175
|
+
|
|
176
|
+
if self.path:
|
|
177
|
+
suffix = self.path.suffix.lower()
|
|
178
|
+
return {
|
|
179
|
+
".wav": "audio/wav",
|
|
180
|
+
".mp3": "audio/mpeg",
|
|
181
|
+
".ogg": "audio/ogg",
|
|
182
|
+
".m4a": "audio/mp4",
|
|
183
|
+
".flac": "audio/flac",
|
|
184
|
+
}.get(suffix, "application/octet-stream")
|
|
185
|
+
return "audio/wav" # default for raw binary data
|
|
186
|
+
|
|
187
|
+
def to_audio_content(
|
|
188
|
+
self,
|
|
189
|
+
mime_type: str | None = None,
|
|
190
|
+
annotations: Annotations | None = None,
|
|
191
|
+
) -> AudioContent:
|
|
192
|
+
if self.path:
|
|
193
|
+
with open(self.path, "rb") as f:
|
|
194
|
+
data = base64.b64encode(f.read()).decode()
|
|
195
|
+
elif self.data is not None:
|
|
196
|
+
data = base64.b64encode(self.data).decode()
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError("No audio data available")
|
|
199
|
+
|
|
200
|
+
return AudioContent(
|
|
201
|
+
type="audio",
|
|
202
|
+
data=data,
|
|
203
|
+
mimeType=mime_type or self._mime_type,
|
|
204
|
+
annotations=annotations or self.annotations,
|
|
205
|
+
)
|