fastmcp 2.11.3__py3-none-any.whl → 2.12.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 +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +139 -64
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +0 -2
- fastmcp/experimental/server/openapi/server.py +0 -2
- fastmcp/experimental/utilities/openapi/parser.py +5 -1
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +2 -0
- fastmcp/resources/resource_manager.py +4 -0
- fastmcp/server/auth/__init__.py +2 -0
- fastmcp/server/auth/auth.py +2 -1
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +24 -12
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/context.py +91 -41
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +3 -3
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +76 -15
- fastmcp/settings.py +16 -1
- fastmcp/tools/tool.py +22 -9
- fastmcp/tools/tool_manager.py +2 -0
- fastmcp/tools/tool_transform.py +39 -10
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +2 -1
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.3.dist-info/RECORD +0 -108
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
+
import json
|
|
6
7
|
import re
|
|
8
|
+
import secrets
|
|
7
9
|
import warnings
|
|
8
10
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
11
|
from contextlib import (
|
|
@@ -69,6 +71,7 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
69
71
|
|
|
70
72
|
if TYPE_CHECKING:
|
|
71
73
|
from fastmcp.client import Client
|
|
74
|
+
from fastmcp.client.sampling import ServerSamplingHandler
|
|
72
75
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
73
76
|
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
74
77
|
from fastmcp.experimental.server.openapi.routing import (
|
|
@@ -166,12 +169,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
166
169
|
streamable_http_path: str | None = None,
|
|
167
170
|
json_response: bool | None = None,
|
|
168
171
|
stateless_http: bool | None = None,
|
|
172
|
+
sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
|
|
173
|
+
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
169
174
|
):
|
|
170
175
|
self.resource_prefix_format: Literal["protocol", "path"] = (
|
|
171
176
|
resource_prefix_format or fastmcp.settings.resource_prefix_format
|
|
172
177
|
)
|
|
173
178
|
|
|
174
179
|
self._additional_http_routes: list[BaseRoute] = []
|
|
180
|
+
self._mounted_servers: list[MountedServer] = []
|
|
175
181
|
self._tool_manager = ToolManager(
|
|
176
182
|
duplicate_behavior=on_duplicate_tools,
|
|
177
183
|
mask_error_details=mask_error_details,
|
|
@@ -192,8 +198,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
192
198
|
lifespan = default_lifespan
|
|
193
199
|
else:
|
|
194
200
|
self._has_lifespan = True
|
|
201
|
+
# Generate random ID if no name provided
|
|
195
202
|
self._mcp_server = LowLevelServer[LifespanResultT](
|
|
196
|
-
name=name or
|
|
203
|
+
name=name or self.generate_name(),
|
|
197
204
|
version=version,
|
|
198
205
|
instructions=instructions,
|
|
199
206
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
@@ -221,7 +228,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
221
228
|
|
|
222
229
|
# Set up MCP protocol handlers
|
|
223
230
|
self._setup_handlers()
|
|
224
|
-
|
|
231
|
+
|
|
232
|
+
# Handle dependencies with deprecation warning
|
|
233
|
+
# TODO: Remove dependencies parameter (deprecated in v2.11.4)
|
|
234
|
+
if dependencies is not None:
|
|
235
|
+
import warnings
|
|
236
|
+
|
|
237
|
+
warnings.warn(
|
|
238
|
+
"The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
|
|
239
|
+
"Please specify dependencies in a fastmcp.json configuration file instead:\n"
|
|
240
|
+
'{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
|
|
241
|
+
f"{json.dumps(dependencies)}\n }}\n}}\n"
|
|
242
|
+
"See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
|
|
243
|
+
DeprecationWarning,
|
|
244
|
+
stacklevel=2,
|
|
245
|
+
)
|
|
246
|
+
self.dependencies = (
|
|
247
|
+
dependencies or fastmcp.settings.server_dependencies
|
|
248
|
+
) # TODO: Remove (deprecated in v2.11.4)
|
|
249
|
+
|
|
250
|
+
self.sampling_handler = sampling_handler
|
|
251
|
+
self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
|
|
225
252
|
|
|
226
253
|
self.include_fastmcp_meta = (
|
|
227
254
|
include_fastmcp_meta
|
|
@@ -444,7 +471,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
444
471
|
Request and returns a Response.
|
|
445
472
|
|
|
446
473
|
Args:
|
|
447
|
-
path: URL path for the route (e.g., "/
|
|
474
|
+
path: URL path for the route (e.g., "/auth/callback")
|
|
448
475
|
methods: List of HTTP methods to support (e.g., ["GET", "POST"])
|
|
449
476
|
name: Optional name for the route (to reference this route with
|
|
450
477
|
Starlette's reverse URL lookup feature)
|
|
@@ -475,8 +502,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
475
502
|
|
|
476
503
|
return decorator
|
|
477
504
|
|
|
505
|
+
def _get_additional_http_routes(self) -> list[BaseRoute]:
|
|
506
|
+
"""Get all additional HTTP routes including from mounted servers.
|
|
507
|
+
|
|
508
|
+
Returns a list of all custom HTTP routes from this server and
|
|
509
|
+
recursively from all mounted servers.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
List of Starlette BaseRoute objects
|
|
513
|
+
"""
|
|
514
|
+
routes = list(self._additional_http_routes)
|
|
515
|
+
|
|
516
|
+
# Recursively get routes from mounted servers
|
|
517
|
+
for mounted_server in self._mounted_servers:
|
|
518
|
+
mounted_routes = mounted_server.server._get_additional_http_routes()
|
|
519
|
+
routes.extend(mounted_routes)
|
|
520
|
+
|
|
521
|
+
return routes
|
|
522
|
+
|
|
478
523
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
479
|
-
logger.debug("Handler called: list_tools")
|
|
524
|
+
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
480
525
|
|
|
481
526
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
482
527
|
tools = await self._list_tools()
|
|
@@ -520,7 +565,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
520
565
|
return await self._apply_middleware(mw_context, _handler)
|
|
521
566
|
|
|
522
567
|
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
523
|
-
logger.debug("Handler called: list_resources")
|
|
568
|
+
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
524
569
|
|
|
525
570
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
526
571
|
resources = await self._list_resources()
|
|
@@ -565,7 +610,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
565
610
|
return await self._apply_middleware(mw_context, _handler)
|
|
566
611
|
|
|
567
612
|
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
568
|
-
logger.debug("Handler called: list_resource_templates")
|
|
613
|
+
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
569
614
|
|
|
570
615
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
571
616
|
templates = await self._list_resource_templates()
|
|
@@ -610,7 +655,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
610
655
|
return await self._apply_middleware(mw_context, _handler)
|
|
611
656
|
|
|
612
657
|
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
613
|
-
logger.debug("Handler called: list_prompts")
|
|
658
|
+
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
|
614
659
|
|
|
615
660
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
616
661
|
prompts = await self._list_prompts()
|
|
@@ -669,7 +714,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
669
714
|
Returns:
|
|
670
715
|
List of MCP Content objects containing the tool results
|
|
671
716
|
"""
|
|
672
|
-
logger.debug(
|
|
717
|
+
logger.debug(
|
|
718
|
+
f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
|
|
719
|
+
)
|
|
673
720
|
|
|
674
721
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
675
722
|
try:
|
|
@@ -711,7 +758,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
711
758
|
|
|
712
759
|
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
|
|
713
760
|
"""
|
|
714
|
-
logger.debug("Handler called: read_resource %s", uri)
|
|
761
|
+
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
|
|
715
762
|
|
|
716
763
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
717
764
|
try:
|
|
@@ -766,7 +813,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
766
813
|
|
|
767
814
|
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
|
|
768
815
|
"""
|
|
769
|
-
logger.debug(
|
|
816
|
+
logger.debug(
|
|
817
|
+
f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
|
|
818
|
+
)
|
|
770
819
|
|
|
771
820
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
772
821
|
try:
|
|
@@ -984,7 +1033,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
984
1033
|
description=description,
|
|
985
1034
|
tags=tags,
|
|
986
1035
|
output_schema=output_schema,
|
|
987
|
-
annotations=annotations,
|
|
1036
|
+
annotations=cast(ToolAnnotations | None, annotations),
|
|
988
1037
|
exclude_args=exclude_args,
|
|
989
1038
|
meta=meta,
|
|
990
1039
|
serializer=self._tool_serializer,
|
|
@@ -1214,7 +1263,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1214
1263
|
mime_type=mime_type,
|
|
1215
1264
|
tags=tags,
|
|
1216
1265
|
enabled=enabled,
|
|
1217
|
-
annotations=annotations,
|
|
1266
|
+
annotations=cast(Annotations | None, annotations),
|
|
1218
1267
|
meta=meta,
|
|
1219
1268
|
)
|
|
1220
1269
|
self.add_template(template)
|
|
@@ -1229,7 +1278,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1229
1278
|
mime_type=mime_type,
|
|
1230
1279
|
tags=tags,
|
|
1231
1280
|
enabled=enabled,
|
|
1232
|
-
annotations=annotations,
|
|
1281
|
+
annotations=cast(Annotations | None, annotations),
|
|
1233
1282
|
meta=meta,
|
|
1234
1283
|
)
|
|
1235
1284
|
self.add_resource(resource)
|
|
@@ -1796,6 +1845,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1796
1845
|
server=server,
|
|
1797
1846
|
resource_prefix_format=self.resource_prefix_format,
|
|
1798
1847
|
)
|
|
1848
|
+
self._mounted_servers.append(mounted_server)
|
|
1799
1849
|
self._tool_manager.mount(mounted_server)
|
|
1800
1850
|
self._resource_manager.mount(mounted_server)
|
|
1801
1851
|
self._prompt_manager.mount(mounted_server)
|
|
@@ -1922,9 +1972,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1922
1972
|
self._prompt_manager.add_prompt(prompt)
|
|
1923
1973
|
|
|
1924
1974
|
if prefix:
|
|
1925
|
-
logger.debug(
|
|
1975
|
+
logger.debug(
|
|
1976
|
+
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|
|
1977
|
+
)
|
|
1926
1978
|
else:
|
|
1927
|
-
logger.debug(f"Imported server {server.name}")
|
|
1979
|
+
logger.debug(f"[{self.name}] Imported server {server.name}")
|
|
1928
1980
|
|
|
1929
1981
|
@classmethod
|
|
1930
1982
|
def from_openapi(
|
|
@@ -2151,6 +2203,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2151
2203
|
|
|
2152
2204
|
return True
|
|
2153
2205
|
|
|
2206
|
+
@classmethod
|
|
2207
|
+
def generate_name(cls, name: str | None = None) -> str:
|
|
2208
|
+
class_name = cls.__name__
|
|
2209
|
+
|
|
2210
|
+
if name is None:
|
|
2211
|
+
return f"{class_name}-{secrets.token_hex(2)}"
|
|
2212
|
+
else:
|
|
2213
|
+
return f"{class_name}-{name}-{secrets.token_hex(2)}"
|
|
2214
|
+
|
|
2154
2215
|
|
|
2155
2216
|
@dataclass
|
|
2156
2217
|
class MountedServer:
|
fastmcp/settings.py
CHANGED
|
@@ -146,6 +146,7 @@ class Settings(BaseSettings):
|
|
|
146
146
|
|
|
147
147
|
test_mode: bool = False
|
|
148
148
|
|
|
149
|
+
log_enabled: bool = True
|
|
149
150
|
log_level: LOG_LEVEL = "INFO"
|
|
150
151
|
|
|
151
152
|
@field_validator("log_level", mode="before")
|
|
@@ -314,12 +315,26 @@ class Settings(BaseSettings):
|
|
|
314
315
|
Whether to include FastMCP meta in the server's MCP responses.
|
|
315
316
|
If True, a `_fastmcp` key will be added to the `meta` field of
|
|
316
317
|
all MCP component responses. This key will contain a dict of
|
|
317
|
-
various FastMCP-specific metadata, such as tags.
|
|
318
|
+
various FastMCP-specific metadata, such as tags.
|
|
318
319
|
"""
|
|
319
320
|
),
|
|
320
321
|
),
|
|
321
322
|
] = True
|
|
322
323
|
|
|
324
|
+
mounted_components_raise_on_load_error: Annotated[
|
|
325
|
+
bool,
|
|
326
|
+
Field(
|
|
327
|
+
default=False,
|
|
328
|
+
description=inspect.cleandoc(
|
|
329
|
+
"""
|
|
330
|
+
If True, errors encountered when loading mounted components (tools, resources, prompts)
|
|
331
|
+
will be raised instead of logged as warnings. This is useful for debugging
|
|
332
|
+
but will interrupt normal operation.
|
|
333
|
+
"""
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
] = False
|
|
337
|
+
|
|
323
338
|
|
|
324
339
|
def __getattr__(name: str):
|
|
325
340
|
"""
|
fastmcp/tools/tool.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import (
|
|
@@ -8,6 +9,7 @@ from typing import (
|
|
|
8
9
|
Annotated,
|
|
9
10
|
Any,
|
|
10
11
|
Generic,
|
|
12
|
+
Literal,
|
|
11
13
|
TypeVar,
|
|
12
14
|
get_type_hints,
|
|
13
15
|
)
|
|
@@ -18,6 +20,7 @@ from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
|
|
18
20
|
from mcp.types import Tool as MCPTool
|
|
19
21
|
from pydantic import Field, PydanticSchemaGenerationError
|
|
20
22
|
|
|
23
|
+
import fastmcp
|
|
21
24
|
from fastmcp.server.dependencies import get_context
|
|
22
25
|
from fastmcp.utilities.components import FastMCPComponent
|
|
23
26
|
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -162,7 +165,7 @@ class Tool(FastMCPComponent):
|
|
|
162
165
|
tags: set[str] | None = None,
|
|
163
166
|
annotations: ToolAnnotations | None = None,
|
|
164
167
|
exclude_args: list[str] | None = None,
|
|
165
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
168
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
166
169
|
serializer: Callable[[Any], str] | None = None,
|
|
167
170
|
meta: dict[str, Any] | None = None,
|
|
168
171
|
enabled: bool | None = None,
|
|
@@ -204,7 +207,7 @@ class Tool(FastMCPComponent):
|
|
|
204
207
|
description: str | None | NotSetT = NotSet,
|
|
205
208
|
tags: set[str] | None = None,
|
|
206
209
|
annotations: ToolAnnotations | None | NotSetT = NotSet,
|
|
207
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
210
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
208
211
|
serializer: Callable[[Any], str] | None = None,
|
|
209
212
|
meta: dict[str, Any] | None | NotSetT = NotSet,
|
|
210
213
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
@@ -242,7 +245,7 @@ class FunctionTool(Tool):
|
|
|
242
245
|
tags: set[str] | None = None,
|
|
243
246
|
annotations: ToolAnnotations | None = None,
|
|
244
247
|
exclude_args: list[str] | None = None,
|
|
245
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
248
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
246
249
|
serializer: Callable[[Any], str] | None = None,
|
|
247
250
|
meta: dict[str, Any] | None = None,
|
|
248
251
|
enabled: bool | None = None,
|
|
@@ -255,16 +258,26 @@ class FunctionTool(Tool):
|
|
|
255
258
|
raise ValueError("You must provide a name for lambda functions")
|
|
256
259
|
|
|
257
260
|
if isinstance(output_schema, NotSetT):
|
|
258
|
-
|
|
261
|
+
final_output_schema = parsed_fn.output_schema
|
|
259
262
|
elif output_schema is False:
|
|
260
|
-
|
|
263
|
+
# Handle False as deprecated synonym for None (deprecated in 2.11.4)
|
|
264
|
+
if fastmcp.settings.deprecation_warnings:
|
|
265
|
+
warnings.warn(
|
|
266
|
+
"Passing output_schema=False is deprecated. Use output_schema=None instead.",
|
|
267
|
+
DeprecationWarning,
|
|
268
|
+
stacklevel=2,
|
|
269
|
+
)
|
|
270
|
+
final_output_schema = None
|
|
271
|
+
else:
|
|
272
|
+
# At this point output_schema is not NotSetT and not False, so it must be dict | None
|
|
273
|
+
final_output_schema = output_schema
|
|
261
274
|
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
262
275
|
|
|
263
276
|
# Validate that explicit schemas are object type for structured content
|
|
264
|
-
if
|
|
265
|
-
if
|
|
277
|
+
if final_output_schema is not None and isinstance(final_output_schema, dict):
|
|
278
|
+
if final_output_schema.get("type") != "object":
|
|
266
279
|
raise ValueError(
|
|
267
|
-
f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {
|
|
280
|
+
f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {final_output_schema!r}'
|
|
268
281
|
)
|
|
269
282
|
|
|
270
283
|
return cls(
|
|
@@ -273,7 +286,7 @@ class FunctionTool(Tool):
|
|
|
273
286
|
title=title,
|
|
274
287
|
description=description or parsed_fn.description,
|
|
275
288
|
parameters=parsed_fn.input_schema,
|
|
276
|
-
output_schema=
|
|
289
|
+
output_schema=final_output_schema,
|
|
277
290
|
annotations=annotations,
|
|
278
291
|
tags=tags or set(),
|
|
279
292
|
serializer=serializer,
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -86,6 +86,8 @@ class ToolManager:
|
|
|
86
86
|
logger.warning(
|
|
87
87
|
f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
88
88
|
)
|
|
89
|
+
if settings.mounted_components_raise_on_load_error:
|
|
90
|
+
raise
|
|
89
91
|
continue
|
|
90
92
|
|
|
91
93
|
# Finally, add local tools, which always take precedence
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from contextvars import ContextVar
|
|
6
7
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Annotated, Any, Literal
|
|
8
|
+
from typing import Annotated, Any, Literal, cast
|
|
8
9
|
|
|
9
10
|
import pydantic_core
|
|
10
11
|
from mcp.types import ToolAnnotations
|
|
@@ -12,6 +13,7 @@ from pydantic import ConfigDict
|
|
|
12
13
|
from pydantic.fields import Field
|
|
13
14
|
from pydantic.functional_validators import BeforeValidator
|
|
14
15
|
|
|
16
|
+
import fastmcp
|
|
15
17
|
from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
|
|
16
18
|
from fastmcp.utilities.components import _convert_set_default_none
|
|
17
19
|
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -27,7 +29,7 @@ logger = get_logger(__name__)
|
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
# Context variable to store current transformed tool
|
|
30
|
-
_current_tool: ContextVar[TransformedTool | None] = ContextVar(
|
|
32
|
+
_current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[assignment]
|
|
31
33
|
"_current_tool", default=None
|
|
32
34
|
)
|
|
33
35
|
|
|
@@ -369,7 +371,7 @@ class TransformedTool(Tool):
|
|
|
369
371
|
transform_fn: Callable[..., Any] | None = None,
|
|
370
372
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
371
373
|
annotations: ToolAnnotations | None | NotSetT = NotSet,
|
|
372
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
374
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
373
375
|
serializer: Callable[[Any], str] | None | NotSetT = NotSet,
|
|
374
376
|
meta: dict[str, Any] | None | NotSetT = NotSet,
|
|
375
377
|
enabled: bool | None = None,
|
|
@@ -437,7 +439,7 @@ class TransformedTool(Tool):
|
|
|
437
439
|
})
|
|
438
440
|
|
|
439
441
|
# Disable structured outputs
|
|
440
|
-
Tool.from_tool(parent, output_schema=
|
|
442
|
+
Tool.from_tool(parent, output_schema=None)
|
|
441
443
|
|
|
442
444
|
# Return ToolResult for full control
|
|
443
445
|
async def custom_output(**kwargs) -> ToolResult:
|
|
@@ -471,8 +473,8 @@ class TransformedTool(Tool):
|
|
|
471
473
|
if output_schema is NotSet:
|
|
472
474
|
# Use smart fallback: try custom function, then parent
|
|
473
475
|
if transform_fn is not None:
|
|
474
|
-
|
|
475
|
-
final_output_schema = parsed_fn.output_schema
|
|
476
|
+
# parsed fn is not none here
|
|
477
|
+
final_output_schema = cast(ParsedFunction, parsed_fn).output_schema
|
|
476
478
|
if final_output_schema is None:
|
|
477
479
|
# Check if function returns ToolResult - if so, don't fall back to parent
|
|
478
480
|
return_annotation = inspect.signature(
|
|
@@ -484,16 +486,25 @@ class TransformedTool(Tool):
|
|
|
484
486
|
final_output_schema = tool.output_schema
|
|
485
487
|
else:
|
|
486
488
|
final_output_schema = tool.output_schema
|
|
489
|
+
elif output_schema is False:
|
|
490
|
+
# Handle False as deprecated synonym for None (deprecated in 2.11.4)
|
|
491
|
+
if fastmcp.settings.deprecation_warnings:
|
|
492
|
+
warnings.warn(
|
|
493
|
+
"Passing output_schema=False is deprecated. Use output_schema=None instead.",
|
|
494
|
+
DeprecationWarning,
|
|
495
|
+
stacklevel=2,
|
|
496
|
+
)
|
|
497
|
+
final_output_schema = None
|
|
487
498
|
else:
|
|
488
|
-
|
|
489
|
-
final_output_schema = output_schema
|
|
499
|
+
final_output_schema = cast(dict | None, output_schema)
|
|
490
500
|
|
|
491
501
|
if transform_fn is None:
|
|
492
502
|
# User wants pure transformation - use forwarding_fn as the main function
|
|
493
503
|
final_fn = forwarding_fn
|
|
494
504
|
final_schema = schema
|
|
495
505
|
else:
|
|
496
|
-
|
|
506
|
+
# parsed fn is not none here
|
|
507
|
+
parsed_fn = cast(ParsedFunction, parsed_fn)
|
|
497
508
|
# User provided custom function - merge schemas
|
|
498
509
|
final_fn = transform_fn
|
|
499
510
|
|
|
@@ -830,12 +841,30 @@ class TransformedTool(Tool):
|
|
|
830
841
|
if "default" in param_schema:
|
|
831
842
|
final_required.discard(param_name)
|
|
832
843
|
|
|
833
|
-
|
|
844
|
+
# Merge $defs from both schemas, with override taking precedence
|
|
845
|
+
merged_defs = base_schema.get("$defs", {}).copy()
|
|
846
|
+
override_defs = override_schema.get("$defs", {})
|
|
847
|
+
|
|
848
|
+
for def_name, def_schema in override_defs.items():
|
|
849
|
+
if def_name in merged_defs:
|
|
850
|
+
base_def = merged_defs[def_name].copy()
|
|
851
|
+
base_def.update(def_schema)
|
|
852
|
+
merged_defs[def_name] = base_def
|
|
853
|
+
else:
|
|
854
|
+
merged_defs[def_name] = def_schema.copy()
|
|
855
|
+
|
|
856
|
+
result = {
|
|
834
857
|
"type": "object",
|
|
835
858
|
"properties": merged_props,
|
|
836
859
|
"required": list(final_required),
|
|
837
860
|
}
|
|
838
861
|
|
|
862
|
+
if merged_defs:
|
|
863
|
+
result["$defs"] = merged_defs
|
|
864
|
+
result = compress_schema(result, prune_defs=True)
|
|
865
|
+
|
|
866
|
+
return result
|
|
867
|
+
|
|
839
868
|
@staticmethod
|
|
840
869
|
def _function_has_kwargs(fn: Callable[..., Any]) -> bool:
|
|
841
870
|
"""Check if function accepts **kwargs.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Authentication utility helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_scopes(value: Any) -> list[str] | None:
|
|
10
|
+
"""Parse scopes from environment variables or settings values.
|
|
11
|
+
|
|
12
|
+
Accepts either a JSON array string, a comma- or space-separated string,
|
|
13
|
+
a list of strings, or ``None``. Returns a list of scopes or ``None`` if
|
|
14
|
+
no value is provided.
|
|
15
|
+
"""
|
|
16
|
+
if value is None or value == "":
|
|
17
|
+
return None if value is None else []
|
|
18
|
+
if isinstance(value, list):
|
|
19
|
+
return [str(v).strip() for v in value if str(v).strip()]
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
value = value.strip()
|
|
22
|
+
if not value:
|
|
23
|
+
return []
|
|
24
|
+
# Try JSON array first
|
|
25
|
+
if value.startswith("["):
|
|
26
|
+
try:
|
|
27
|
+
data = json.loads(value)
|
|
28
|
+
if isinstance(data, list):
|
|
29
|
+
return [str(v).strip() for v in data if str(v).strip()]
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
# Fallback to comma/space separated list
|
|
33
|
+
return [s.strip() for s in value.replace(",", " ").split() if s.strip()]
|
|
34
|
+
return value
|