fastmcp 2.3.3__py3-none-any.whl → 2.3.4__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/client/__init__.py +2 -0
- fastmcp/client/client.py +84 -21
- fastmcp/client/transports.py +53 -28
- fastmcp/exceptions.py +2 -0
- fastmcp/prompts/prompt.py +12 -6
- fastmcp/resources/resource_manager.py +22 -1
- fastmcp/resources/template.py +21 -17
- fastmcp/resources/types.py +25 -27
- fastmcp/server/openapi.py +14 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +73 -53
- fastmcp/settings.py +55 -29
- fastmcp/tools/tool.py +45 -45
- fastmcp/tools/tool_manager.py +27 -2
- fastmcp/utilities/exceptions.py +49 -0
- fastmcp/utilities/json_schema.py +78 -17
- fastmcp/utilities/logging.py +11 -6
- fastmcp/utilities/openapi.py +122 -7
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.4.dist-info}/METADATA +2 -2
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.4.dist-info}/RECORD +23 -22
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.4.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/proxy.py
CHANGED
|
@@ -18,7 +18,7 @@ from mcp.types import (
|
|
|
18
18
|
from pydantic.networks import AnyUrl
|
|
19
19
|
|
|
20
20
|
from fastmcp.client import Client
|
|
21
|
-
from fastmcp.exceptions import NotFoundError
|
|
21
|
+
from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
|
|
22
22
|
from fastmcp.prompts import Prompt, PromptMessage
|
|
23
23
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
24
24
|
from fastmcp.server.context import Context
|
|
@@ -64,7 +64,7 @@ class ProxyTool(Tool):
|
|
|
64
64
|
arguments=arguments,
|
|
65
65
|
)
|
|
66
66
|
if result.isError:
|
|
67
|
-
raise
|
|
67
|
+
raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
|
|
68
68
|
return result.content
|
|
69
69
|
|
|
70
70
|
|
|
@@ -97,7 +97,7 @@ class ProxyResource(Resource):
|
|
|
97
97
|
elif isinstance(result[0], BlobResourceContents):
|
|
98
98
|
return result[0].blob
|
|
99
99
|
else:
|
|
100
|
-
raise
|
|
100
|
+
raise ResourceError(f"Unsupported content type: {type(result[0])}")
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
class ProxyTemplate(ResourceTemplate):
|
|
@@ -138,7 +138,7 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
138
138
|
elif isinstance(result[0], BlobResourceContents):
|
|
139
139
|
value = result[0].blob
|
|
140
140
|
else:
|
|
141
|
-
raise
|
|
141
|
+
raise ResourceError(f"Unsupported content type: {type(result[0])}")
|
|
142
142
|
|
|
143
143
|
return ProxyResource(
|
|
144
144
|
client=self._client,
|
fastmcp/server/server.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
-
import inspect
|
|
7
6
|
import warnings
|
|
8
7
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
8
|
from contextlib import (
|
|
@@ -20,7 +19,7 @@ import pydantic
|
|
|
20
19
|
import uvicorn
|
|
21
20
|
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
|
22
21
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
23
|
-
from mcp.server.lowlevel.server import LifespanResultT
|
|
22
|
+
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
24
23
|
from mcp.server.lowlevel.server import Server as MCPServer
|
|
25
24
|
from mcp.server.stdio import stdio_server
|
|
26
25
|
from mcp.types import (
|
|
@@ -44,7 +43,7 @@ from starlette.routing import BaseRoute, Route
|
|
|
44
43
|
|
|
45
44
|
import fastmcp.server
|
|
46
45
|
import fastmcp.settings
|
|
47
|
-
from fastmcp.exceptions import NotFoundError
|
|
46
|
+
from fastmcp.exceptions import NotFoundError
|
|
48
47
|
from fastmcp.prompts import Prompt, PromptManager
|
|
49
48
|
from fastmcp.prompts.prompt import PromptResult
|
|
50
49
|
from fastmcp.resources import Resource, ResourceManager
|
|
@@ -54,7 +53,7 @@ from fastmcp.tools import ToolManager
|
|
|
54
53
|
from fastmcp.tools.tool import Tool
|
|
55
54
|
from fastmcp.utilities.cache import TimedCache
|
|
56
55
|
from fastmcp.utilities.decorators import DecoratedFunction
|
|
57
|
-
from fastmcp.utilities.logging import
|
|
56
|
+
from fastmcp.utilities.logging import get_logger
|
|
58
57
|
|
|
59
58
|
if TYPE_CHECKING:
|
|
60
59
|
from fastmcp.client import Client
|
|
@@ -63,6 +62,8 @@ if TYPE_CHECKING:
|
|
|
63
62
|
|
|
64
63
|
logger = get_logger(__name__)
|
|
65
64
|
|
|
65
|
+
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
66
|
+
|
|
66
67
|
|
|
67
68
|
@asynccontextmanager
|
|
68
69
|
async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
@@ -107,40 +108,52 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
107
108
|
| None
|
|
108
109
|
) = None,
|
|
109
110
|
tags: set[str] | None = None,
|
|
111
|
+
dependencies: list[str] | None = None,
|
|
110
112
|
tool_serializer: Callable[[Any], str] | None = None,
|
|
113
|
+
cache_expiration_seconds: float | None = None,
|
|
114
|
+
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
115
|
+
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
116
|
+
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
111
117
|
**settings: Any,
|
|
112
118
|
):
|
|
113
|
-
|
|
119
|
+
if settings:
|
|
120
|
+
# TODO: remove settings. Deprecated since 2.3.4
|
|
121
|
+
warnings.warn(
|
|
122
|
+
"Passing runtime and transport-specific settings as kwargs "
|
|
123
|
+
"to the FastMCP constructor is deprecated (as of 2.3.4), "
|
|
124
|
+
"including most transport settings. If possible, provide settings when calling "
|
|
125
|
+
"run() instead.",
|
|
126
|
+
DeprecationWarning,
|
|
127
|
+
stacklevel=2,
|
|
128
|
+
)
|
|
114
129
|
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
130
|
+
|
|
131
|
+
self.tags: set[str] = tags or set()
|
|
132
|
+
self.dependencies = dependencies
|
|
115
133
|
self._cache = TimedCache(
|
|
116
|
-
expiration=datetime.timedelta(
|
|
117
|
-
seconds=self.settings.cache_expiration_seconds
|
|
118
|
-
)
|
|
134
|
+
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
|
119
135
|
)
|
|
120
|
-
|
|
121
136
|
self._mounted_servers: dict[str, MountedServer] = {}
|
|
137
|
+
self._additional_http_routes: list[BaseRoute] = []
|
|
138
|
+
self._tool_manager = ToolManager(
|
|
139
|
+
duplicate_behavior=on_duplicate_tools,
|
|
140
|
+
serializer=tool_serializer,
|
|
141
|
+
)
|
|
142
|
+
self._resource_manager = ResourceManager(
|
|
143
|
+
duplicate_behavior=on_duplicate_resources
|
|
144
|
+
)
|
|
145
|
+
self._prompt_manager = PromptManager(duplicate_behavior=on_duplicate_prompts)
|
|
122
146
|
|
|
123
147
|
if lifespan is None:
|
|
124
148
|
self._has_lifespan = False
|
|
125
149
|
lifespan = default_lifespan
|
|
126
150
|
else:
|
|
127
151
|
self._has_lifespan = True
|
|
128
|
-
|
|
129
152
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
130
153
|
name=name or "FastMCP",
|
|
131
154
|
instructions=instructions,
|
|
132
155
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
133
156
|
)
|
|
134
|
-
self._tool_manager = ToolManager(
|
|
135
|
-
duplicate_behavior=self.settings.on_duplicate_tools,
|
|
136
|
-
serializer=tool_serializer,
|
|
137
|
-
)
|
|
138
|
-
self._resource_manager = ResourceManager(
|
|
139
|
-
duplicate_behavior=self.settings.on_duplicate_resources
|
|
140
|
-
)
|
|
141
|
-
self._prompt_manager = PromptManager(
|
|
142
|
-
duplicate_behavior=self.settings.on_duplicate_prompts
|
|
143
|
-
)
|
|
144
157
|
|
|
145
158
|
if (self.settings.auth is not None) != (auth_server_provider is not None):
|
|
146
159
|
# TODO: after we support separate authorization servers (see
|
|
@@ -150,15 +163,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
150
163
|
)
|
|
151
164
|
self._auth_server_provider = auth_server_provider
|
|
152
165
|
|
|
153
|
-
self._additional_http_routes: list[BaseRoute] = []
|
|
154
|
-
self.dependencies = self.settings.dependencies
|
|
155
|
-
|
|
156
166
|
# Set up MCP protocol handlers
|
|
157
167
|
self._setup_handlers()
|
|
158
168
|
|
|
159
|
-
# Configure logging
|
|
160
|
-
configure_logging(self.settings.log_level)
|
|
161
|
-
|
|
162
169
|
def __repr__(self) -> str:
|
|
163
170
|
return f"{type(self).__name__}({self.name!r})"
|
|
164
171
|
|
|
@@ -378,16 +385,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
378
385
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
379
386
|
if self._resource_manager.has_resource(uri):
|
|
380
387
|
resource = await self._resource_manager.get_resource(uri)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
except Exception as e:
|
|
389
|
-
logger.error(f"Error reading resource {uri}: {e}")
|
|
390
|
-
raise ResourceError(str(e))
|
|
388
|
+
content = await self._resource_manager.read_resource(uri)
|
|
389
|
+
return [
|
|
390
|
+
ReadResourceContents(
|
|
391
|
+
content=content,
|
|
392
|
+
mime_type=resource.mime_type,
|
|
393
|
+
)
|
|
394
|
+
]
|
|
391
395
|
else:
|
|
392
396
|
for server in self._mounted_servers.values():
|
|
393
397
|
if server.match_resource(str(uri)):
|
|
@@ -414,7 +418,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
414
418
|
for server in self._mounted_servers.values():
|
|
415
419
|
if server.match_prompt(name):
|
|
416
420
|
new_key = server.strip_prompt_prefix(name)
|
|
417
|
-
|
|
421
|
+
return await server.server._mcp_get_prompt(new_key, arguments)
|
|
418
422
|
else:
|
|
419
423
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
420
424
|
|
|
@@ -450,6 +454,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
450
454
|
)
|
|
451
455
|
self._cache.clear()
|
|
452
456
|
|
|
457
|
+
def remove_tool(self, name: str) -> None:
|
|
458
|
+
"""Remove a tool from the server.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
name: The name of the tool to remove
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
NotFoundError: If the tool is not found
|
|
465
|
+
"""
|
|
466
|
+
self._tool_manager.remove_tool(name)
|
|
467
|
+
self._cache.clear()
|
|
468
|
+
|
|
453
469
|
def tool(
|
|
454
470
|
self,
|
|
455
471
|
name: str | None = None,
|
|
@@ -715,7 +731,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
715
731
|
await self._mcp_server.run(
|
|
716
732
|
read_stream,
|
|
717
733
|
write_stream,
|
|
718
|
-
self._mcp_server.create_initialization_options(
|
|
734
|
+
self._mcp_server.create_initialization_options(
|
|
735
|
+
NotificationOptions(tools_changed=True)
|
|
736
|
+
),
|
|
719
737
|
)
|
|
720
738
|
|
|
721
739
|
async def run_http_async(
|
|
@@ -764,15 +782,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
764
782
|
uvicorn_config: dict | None = None,
|
|
765
783
|
) -> None:
|
|
766
784
|
"""Run the server using SSE transport."""
|
|
785
|
+
|
|
786
|
+
# Deprecated since 2.3.2
|
|
767
787
|
warnings.warn(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
modern (non-SSE) alternative, or create an SSE app with
|
|
772
|
-
`fastmcp.server.http.create_sse_app` and run it directly.
|
|
773
|
-
"""
|
|
774
|
-
),
|
|
788
|
+
"The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
|
|
789
|
+
"modern (non-SSE) alternative, or create an SSE app with "
|
|
790
|
+
"`fastmcp.server.http.create_sse_app` and run it directly.",
|
|
775
791
|
DeprecationWarning,
|
|
792
|
+
stacklevel=2,
|
|
776
793
|
)
|
|
777
794
|
await self.run_http_async(
|
|
778
795
|
transport="sse",
|
|
@@ -797,14 +814,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
797
814
|
message_path: The path to the message endpoint
|
|
798
815
|
middleware: A list of middleware to apply to the app
|
|
799
816
|
"""
|
|
817
|
+
# Deprecated since 2.3.2
|
|
800
818
|
warnings.warn(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
The sse_app method is deprecated. Use http_app as a modern (non-SSE)
|
|
804
|
-
alternative, or call `fastmcp.server.http.create_sse_app` directly.
|
|
805
|
-
"""
|
|
806
|
-
),
|
|
819
|
+
"The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
|
|
820
|
+
"alternative, or call `fastmcp.server.http.create_sse_app` directly.",
|
|
807
821
|
DeprecationWarning,
|
|
822
|
+
stacklevel=2,
|
|
808
823
|
)
|
|
809
824
|
return create_sse_app(
|
|
810
825
|
server=self,
|
|
@@ -829,9 +844,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
829
844
|
path: The path to the StreamableHTTP endpoint
|
|
830
845
|
middleware: A list of middleware to apply to the app
|
|
831
846
|
"""
|
|
847
|
+
# Deprecated since 2.3.2
|
|
832
848
|
warnings.warn(
|
|
833
|
-
"The streamable_http_app method is deprecated. Use http_app() instead.",
|
|
849
|
+
"The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
|
|
834
850
|
DeprecationWarning,
|
|
851
|
+
stacklevel=2,
|
|
835
852
|
)
|
|
836
853
|
return self.http_app(path=path, middleware=middleware)
|
|
837
854
|
|
|
@@ -886,9 +903,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
886
903
|
path: str | None = None,
|
|
887
904
|
uvicorn_config: dict | None = None,
|
|
888
905
|
) -> None:
|
|
906
|
+
# Deprecated since 2.3.2
|
|
889
907
|
warnings.warn(
|
|
890
|
-
"The run_streamable_http_async method is deprecated
|
|
908
|
+
"The run_streamable_http_async method is deprecated (as of 2.3.2). "
|
|
909
|
+
"Use run_http_async instead.",
|
|
891
910
|
DeprecationWarning,
|
|
911
|
+
stacklevel=2,
|
|
892
912
|
)
|
|
893
913
|
await self.run_http_async(
|
|
894
914
|
transport="streamable-http",
|
fastmcp/settings.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import TYPE_CHECKING, Annotated, Literal
|
|
4
5
|
|
|
5
6
|
from mcp.server.auth.settings import AuthSettings
|
|
6
|
-
from pydantic import Field
|
|
7
|
+
from pydantic import Field, model_validator
|
|
7
8
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
|
+
from typing_extensions import Self
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
pass
|
|
@@ -27,16 +29,46 @@ class Settings(BaseSettings):
|
|
|
27
29
|
|
|
28
30
|
test_mode: bool = False
|
|
29
31
|
log_level: LOG_LEVEL = "INFO"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
client_raise_first_exceptiongroup_error: Annotated[
|
|
33
|
+
bool,
|
|
34
|
+
Field(
|
|
35
|
+
default=True,
|
|
36
|
+
description=inspect.cleandoc(
|
|
37
|
+
"""
|
|
38
|
+
Many MCP components operate in anyio taskgroups, and raise
|
|
39
|
+
ExceptionGroups instead of exceptions. If this setting is True, FastMCP Clients
|
|
40
|
+
will `raise` the first error in any ExceptionGroup instead of raising
|
|
41
|
+
the ExceptionGroup as a whole. This is useful for debugging, but may
|
|
42
|
+
mask other errors.
|
|
43
|
+
"""
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
] = True
|
|
47
|
+
tool_attempt_parse_json_args: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
Field(
|
|
50
|
+
default=False,
|
|
51
|
+
description=inspect.cleandoc(
|
|
52
|
+
"""
|
|
53
|
+
Note: this enables a legacy behavior. If True, will attempt to parse
|
|
54
|
+
stringified JSON lists and objects strings in tool arguments before
|
|
55
|
+
passing them to the tool. This is an old behavior that can create
|
|
56
|
+
unexpected type coercion issues, but may be helpful for less powerful
|
|
57
|
+
LLMs that stringify JSON instead of passing actual lists and objects.
|
|
58
|
+
Defaults to False.
|
|
59
|
+
"""
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
] = False
|
|
63
|
+
|
|
64
|
+
@model_validator(mode="after")
|
|
65
|
+
def setup_logging(self) -> Self:
|
|
66
|
+
"""Finalize the settings."""
|
|
67
|
+
from fastmcp.utilities.logging import configure_logging
|
|
68
|
+
|
|
69
|
+
configure_logging(self.log_level)
|
|
70
|
+
|
|
71
|
+
return self
|
|
40
72
|
|
|
41
73
|
|
|
42
74
|
class ServerSettings(BaseSettings):
|
|
@@ -54,7 +86,10 @@ class ServerSettings(BaseSettings):
|
|
|
54
86
|
nested_model_default_partial_update=True,
|
|
55
87
|
)
|
|
56
88
|
|
|
57
|
-
log_level:
|
|
89
|
+
log_level: Annotated[
|
|
90
|
+
LOG_LEVEL,
|
|
91
|
+
Field(default_factory=lambda: Settings().log_level),
|
|
92
|
+
]
|
|
58
93
|
|
|
59
94
|
# HTTP settings
|
|
60
95
|
host: str = "127.0.0.1"
|
|
@@ -73,10 +108,13 @@ class ServerSettings(BaseSettings):
|
|
|
73
108
|
# prompt settings
|
|
74
109
|
on_duplicate_prompts: DuplicateBehavior = "warn"
|
|
75
110
|
|
|
76
|
-
dependencies:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
dependencies: Annotated[
|
|
112
|
+
list[str],
|
|
113
|
+
Field(
|
|
114
|
+
default_factory=list,
|
|
115
|
+
description="List of dependencies to install in the server environment",
|
|
116
|
+
),
|
|
117
|
+
] = []
|
|
80
118
|
|
|
81
119
|
# cache settings (for checking mounted servers)
|
|
82
120
|
cache_expiration_seconds: float = 0
|
|
@@ -90,16 +128,4 @@ class ServerSettings(BaseSettings):
|
|
|
90
128
|
)
|
|
91
129
|
|
|
92
130
|
|
|
93
|
-
class ClientSettings(BaseSettings):
|
|
94
|
-
"""FastMCP client settings."""
|
|
95
|
-
|
|
96
|
-
model_config = SettingsConfigDict(
|
|
97
|
-
env_prefix="FASTMCP_CLIENT_",
|
|
98
|
-
env_file=".env",
|
|
99
|
-
extra="ignore",
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
|
|
103
|
-
|
|
104
|
-
|
|
105
131
|
settings = Settings()
|
fastmcp/tools/tool.py
CHANGED
|
@@ -11,9 +11,8 @@ from mcp.types import Tool as MCPTool
|
|
|
11
11
|
from pydantic import BaseModel, BeforeValidator, Field
|
|
12
12
|
|
|
13
13
|
import fastmcp
|
|
14
|
-
from fastmcp.exceptions import ToolError
|
|
15
14
|
from fastmcp.server.dependencies import get_context
|
|
16
|
-
from fastmcp.utilities.json_schema import
|
|
15
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
17
16
|
from fastmcp.utilities.logging import get_logger
|
|
18
17
|
from fastmcp.utilities.types import (
|
|
19
18
|
Image,
|
|
@@ -82,7 +81,11 @@ class Tool(BaseModel):
|
|
|
82
81
|
|
|
83
82
|
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
84
83
|
if context_kwarg:
|
|
85
|
-
|
|
84
|
+
prune_params = [context_kwarg]
|
|
85
|
+
else:
|
|
86
|
+
prune_params = None
|
|
87
|
+
|
|
88
|
+
schema = compress_schema(schema, prune_params=prune_params)
|
|
86
89
|
|
|
87
90
|
return cls(
|
|
88
91
|
fn=fn,
|
|
@@ -102,48 +105,45 @@ class Tool(BaseModel):
|
|
|
102
105
|
|
|
103
106
|
arguments = arguments.copy()
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return _convert_to_content(result, serializer=self.serializer)
|
|
145
|
-
except Exception as e:
|
|
146
|
-
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
108
|
+
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
109
|
+
if context_kwarg and context_kwarg not in arguments:
|
|
110
|
+
arguments[context_kwarg] = get_context()
|
|
111
|
+
|
|
112
|
+
if fastmcp.settings.settings.tool_attempt_parse_json_args:
|
|
113
|
+
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
|
114
|
+
# being passed in as JSON inside a string rather than an actual list.
|
|
115
|
+
#
|
|
116
|
+
# Claude desktop is prone to this - in fact it seems incapable of NOT doing
|
|
117
|
+
# this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
|
|
118
|
+
# which can be pre-parsed here.
|
|
119
|
+
signature = inspect.signature(self.fn)
|
|
120
|
+
for param_name in self.parameters["properties"]:
|
|
121
|
+
arg = arguments.get(param_name, None)
|
|
122
|
+
# if not in signature, we won't have annotations, so skip logic
|
|
123
|
+
if param_name not in signature.parameters:
|
|
124
|
+
continue
|
|
125
|
+
# if not a string, we won't have a JSON to parse, so skip logic
|
|
126
|
+
if not isinstance(arg, str):
|
|
127
|
+
continue
|
|
128
|
+
# skip if the type is a simple type (int, float, bool)
|
|
129
|
+
if signature.parameters[param_name].annotation in (
|
|
130
|
+
int,
|
|
131
|
+
float,
|
|
132
|
+
bool,
|
|
133
|
+
):
|
|
134
|
+
continue
|
|
135
|
+
try:
|
|
136
|
+
arguments[param_name] = json.loads(arg)
|
|
137
|
+
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
type_adapter = get_cached_typeadapter(self.fn)
|
|
142
|
+
result = type_adapter.validate_python(arguments)
|
|
143
|
+
if inspect.isawaitable(result):
|
|
144
|
+
result = await result
|
|
145
|
+
|
|
146
|
+
return _convert_to_content(result, serializer=self.serializer)
|
|
147
147
|
|
|
148
148
|
def to_mcp_tool(self, **overrides: Any) -> MCPTool:
|
|
149
149
|
kwargs = {
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
5
5
|
|
|
6
6
|
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
|
7
7
|
|
|
8
|
-
from fastmcp.exceptions import NotFoundError
|
|
8
|
+
from fastmcp.exceptions import NotFoundError, ToolError
|
|
9
9
|
from fastmcp.settings import DuplicateBehavior
|
|
10
10
|
from fastmcp.tools.tool import Tool
|
|
11
11
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -94,6 +94,20 @@ class ToolManager:
|
|
|
94
94
|
self._tools[key] = tool
|
|
95
95
|
return tool
|
|
96
96
|
|
|
97
|
+
def remove_tool(self, key: str) -> None:
|
|
98
|
+
"""Remove a tool from the server.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
key: The key of the tool to remove
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
NotFoundError: If the tool is not found
|
|
105
|
+
"""
|
|
106
|
+
if key in self._tools:
|
|
107
|
+
del self._tools[key]
|
|
108
|
+
else:
|
|
109
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
110
|
+
|
|
97
111
|
async def call_tool(
|
|
98
112
|
self, key: str, arguments: dict[str, Any]
|
|
99
113
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
@@ -102,4 +116,15 @@ class ToolManager:
|
|
|
102
116
|
if not tool:
|
|
103
117
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
104
118
|
|
|
105
|
-
|
|
119
|
+
try:
|
|
120
|
+
return await tool.run(arguments)
|
|
121
|
+
|
|
122
|
+
# raise ToolErrors as-is
|
|
123
|
+
except ToolError as e:
|
|
124
|
+
logger.exception(f"Error calling tool {key!r}: {e}")
|
|
125
|
+
raise e
|
|
126
|
+
|
|
127
|
+
# raise other exceptions as ToolErrors without revealing internal details
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.exception(f"Error calling tool {key!r}: {e}")
|
|
130
|
+
raise ToolError(f"Error calling tool {key!r}") from e
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import mcp.types
|
|
6
|
+
from exceptiongroup import BaseExceptionGroup
|
|
7
|
+
from mcp import McpError
|
|
8
|
+
|
|
9
|
+
import fastmcp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def iter_exc(group: BaseExceptionGroup):
|
|
13
|
+
for exc in group.exceptions:
|
|
14
|
+
if isinstance(exc, BaseExceptionGroup):
|
|
15
|
+
yield from iter_exc(exc)
|
|
16
|
+
else:
|
|
17
|
+
yield exc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _exception_handler(group: BaseExceptionGroup):
|
|
21
|
+
for leaf in iter_exc(group):
|
|
22
|
+
if isinstance(leaf, httpx.ConnectTimeout):
|
|
23
|
+
raise McpError(
|
|
24
|
+
error=mcp.types.ErrorData(
|
|
25
|
+
code=httpx.codes.REQUEST_TIMEOUT,
|
|
26
|
+
message="Timed out while waiting for response.",
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
raise leaf
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# this catch handler is used to catch taskgroup exception groups and raise the
|
|
33
|
+
# first exception. This allows more sane debugging.
|
|
34
|
+
_catch_handlers: Mapping[
|
|
35
|
+
type[BaseException] | Iterable[type[BaseException]],
|
|
36
|
+
Callable[[BaseExceptionGroup[Any]], Any],
|
|
37
|
+
] = {
|
|
38
|
+
Exception: _exception_handler,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_catch_handlers() -> Mapping[
|
|
43
|
+
type[BaseException] | Iterable[type[BaseException]],
|
|
44
|
+
Callable[[BaseExceptionGroup[Any]], Any],
|
|
45
|
+
]:
|
|
46
|
+
if fastmcp.settings.settings.client_raise_first_exceptiongroup_error:
|
|
47
|
+
return _catch_handlers
|
|
48
|
+
else:
|
|
49
|
+
return {}
|