Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
6ea503a
1
Parent(s): 5e329a1
Remove legacy json parsing
Browse filesBecause the server now validates inputs, its no longer viable to provide a JSON string where e.g. an array is expected, so we can entirely remove FastMCP's "legacy json parsing" support from 1.x.
This is a breaking change (for legacy opt-in behavior)
- docs/servers/tools.mdx +0 -10
- src/fastmcp/settings.py +0 -17
- src/fastmcp/tools/tool.py +0 -31
- tests/server/test_mount.py +1 -1
- tests/tools/test_tool.py +0 -183
- tests/tools/test_tool_manager.py +0 -33
docs/servers/tools.mdx
CHANGED
|
@@ -856,13 +856,3 @@ def calculate_sum(a: int, b: int) -> int:
|
|
| 856 |
|
| 857 |
mcp.remove_tool("calculate_sum")
|
| 858 |
```
|
| 859 |
-
|
| 860 |
-
### Legacy JSON Parsing
|
| 861 |
-
|
| 862 |
-
<VersionBadge version="2.2.10" />
|
| 863 |
-
|
| 864 |
-
FastMCP 1.0 and < 2.2.10 relied on a crutch that attempted to work around LLM limitations by automatically parsing stringified JSON in tool arguments (e.g., converting `"[1,2,3]"` to `[1,2,3]`). As of FastMCP 2.2.10, this behavior is disabled by default because it circumvents type validation and can lead to unexpected type coercion issues (e.g. parsing "true" as a bool and attempting to call a tool that expected a string, which would fail type validation).
|
| 865 |
-
|
| 866 |
-
Most modern LLMs correctly format JSON, but if working with models that unnecessarily stringify JSON (as was the case with Claude Desktop in late 2024), you can re-enable this behavior on your server by setting the environment variable `FASTMCP_TOOL_ATTEMPT_PARSE_JSON_ARGS=1`.
|
| 867 |
-
|
| 868 |
-
We strongly recommend leaving this disabled unless necessary.
|
|
|
|
| 856 |
|
| 857 |
mcp.remove_tool("calculate_sum")
|
| 858 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/fastmcp/settings.py
CHANGED
|
@@ -154,23 +154,6 @@ class Settings(BaseSettings):
|
|
| 154 |
),
|
| 155 |
] = "path"
|
| 156 |
|
| 157 |
-
tool_attempt_parse_json_args: Annotated[
|
| 158 |
-
bool,
|
| 159 |
-
Field(
|
| 160 |
-
default=False,
|
| 161 |
-
description=inspect.cleandoc(
|
| 162 |
-
"""
|
| 163 |
-
Note: this enables a legacy behavior. If True, will attempt to parse
|
| 164 |
-
stringified JSON lists and objects strings in tool arguments before
|
| 165 |
-
passing them to the tool. This is an old behavior that can create
|
| 166 |
-
unexpected type coercion issues, but may be helpful for less powerful
|
| 167 |
-
LLMs that stringify JSON instead of passing actual lists and objects.
|
| 168 |
-
Defaults to False.
|
| 169 |
-
"""
|
| 170 |
-
),
|
| 171 |
-
),
|
| 172 |
-
] = False
|
| 173 |
-
|
| 174 |
client_init_timeout: Annotated[
|
| 175 |
float | None,
|
| 176 |
Field(
|
|
|
|
| 154 |
),
|
| 155 |
] = "path"
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
client_init_timeout: Annotated[
|
| 158 |
float | None,
|
| 159 |
Field(
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
-
import json
|
| 5 |
from collections.abc import Callable
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from typing import TYPE_CHECKING, Any
|
|
@@ -11,7 +10,6 @@ from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
|
| 11 |
from mcp.types import Tool as MCPTool
|
| 12 |
from pydantic import Field
|
| 13 |
|
| 14 |
-
import fastmcp
|
| 15 |
from fastmcp.server.dependencies import get_context
|
| 16 |
from fastmcp.utilities.components import FastMCPComponent
|
| 17 |
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -168,35 +166,6 @@ class FunctionTool(Tool):
|
|
| 168 |
if context_kwarg and context_kwarg not in arguments:
|
| 169 |
arguments[context_kwarg] = get_context()
|
| 170 |
|
| 171 |
-
if fastmcp.settings.tool_attempt_parse_json_args:
|
| 172 |
-
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
| 173 |
-
# being passed in as JSON inside a string rather than an actual list.
|
| 174 |
-
#
|
| 175 |
-
# Claude desktop is prone to this - in fact it seems incapable of NOT doing
|
| 176 |
-
# this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
|
| 177 |
-
# which can be pre-parsed here.
|
| 178 |
-
signature = inspect.signature(self.fn)
|
| 179 |
-
for param_name in self.parameters["properties"]:
|
| 180 |
-
arg = arguments.get(param_name, None)
|
| 181 |
-
# if not in signature, we won't have annotations, so skip logic
|
| 182 |
-
if param_name not in signature.parameters:
|
| 183 |
-
continue
|
| 184 |
-
# if not a string, we won't have a JSON to parse, so skip logic
|
| 185 |
-
if not isinstance(arg, str):
|
| 186 |
-
continue
|
| 187 |
-
# skip if the type is a simple type (int, float, bool)
|
| 188 |
-
if signature.parameters[param_name].annotation in (
|
| 189 |
-
int,
|
| 190 |
-
float,
|
| 191 |
-
bool,
|
| 192 |
-
):
|
| 193 |
-
continue
|
| 194 |
-
try:
|
| 195 |
-
arguments[param_name] = json.loads(arg)
|
| 196 |
-
|
| 197 |
-
except json.JSONDecodeError:
|
| 198 |
-
pass
|
| 199 |
-
|
| 200 |
type_adapter = get_cached_typeadapter(self.fn)
|
| 201 |
result = type_adapter.validate_python(arguments)
|
| 202 |
if inspect.isawaitable(result):
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import inspect
|
|
|
|
| 4 |
from collections.abc import Callable
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from typing import TYPE_CHECKING, Any
|
|
|
|
| 10 |
from mcp.types import Tool as MCPTool
|
| 11 |
from pydantic import Field
|
| 12 |
|
|
|
|
| 13 |
from fastmcp.server.dependencies import get_context
|
| 14 |
from fastmcp.utilities.components import FastMCPComponent
|
| 15 |
from fastmcp.utilities.json_schema import compress_schema
|
|
|
|
| 166 |
if context_kwarg and context_kwarg not in arguments:
|
| 167 |
arguments[context_kwarg] = get_context()
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
type_adapter = get_cached_typeadapter(self.fn)
|
| 170 |
result = type_adapter.validate_python(arguments)
|
| 171 |
if inspect.isawaitable(result):
|
tests/server/test_mount.py
CHANGED
|
@@ -962,4 +962,4 @@ class TestAsProxyKwarg:
|
|
| 962 |
assert len(lifespan_check) > 0
|
| 963 |
# in the present implementation the sub server will be invoked 3 times
|
| 964 |
# to call its tool
|
| 965 |
-
assert lifespan_check
|
|
|
|
| 962 |
assert len(lifespan_check) > 0
|
| 963 |
# in the present implementation the sub server will be invoked 3 times
|
| 964 |
# to call its tool
|
| 965 |
+
assert lifespan_check.count("start") >= 2
|
tests/tools/test_tool.py
CHANGED
|
@@ -10,11 +10,7 @@ from mcp.types import (
|
|
| 10 |
)
|
| 11 |
from pydantic import AnyUrl, BaseModel
|
| 12 |
|
| 13 |
-
from fastmcp import FastMCP
|
| 14 |
-
from fastmcp.client import Client
|
| 15 |
-
from fastmcp.exceptions import ToolError
|
| 16 |
from fastmcp.tools.tool import Tool, _convert_to_content
|
| 17 |
-
from fastmcp.utilities.tests import temporary_settings
|
| 18 |
from fastmcp.utilities.types import Audio, File, Image
|
| 19 |
|
| 20 |
|
|
@@ -244,185 +240,6 @@ class TestToolFromFunction:
|
|
| 244 |
assert result[0].text == "Custom serializer: 15"
|
| 245 |
|
| 246 |
|
| 247 |
-
class TestLegacyToolJsonParsing:
|
| 248 |
-
"""Tests for Tool's JSON pre-parsing functionality."""
|
| 249 |
-
|
| 250 |
-
@pytest.fixture(autouse=True)
|
| 251 |
-
def enable_legacy_json_parsing(self):
|
| 252 |
-
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 253 |
-
yield
|
| 254 |
-
|
| 255 |
-
async def test_json_string_arguments(self):
|
| 256 |
-
"""Test that JSON string arguments are parsed and validated correctly"""
|
| 257 |
-
|
| 258 |
-
def simple_func(x: int, y: list[str]) -> str:
|
| 259 |
-
return f"{x}-{','.join(y)}"
|
| 260 |
-
|
| 261 |
-
# Create a tool to use its JSON pre-parsing logic
|
| 262 |
-
tool = Tool.from_function(simple_func)
|
| 263 |
-
|
| 264 |
-
# Prepare arguments where some are JSON strings
|
| 265 |
-
json_args = {
|
| 266 |
-
"x": 1,
|
| 267 |
-
"y": '["a", "b", "c"]', # JSON string
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
# Run the tool which will do JSON parsing
|
| 271 |
-
result = await tool.run(json_args)
|
| 272 |
-
assert result[0].text == "1-a,b,c" # type: ignore[attr-dict]
|
| 273 |
-
|
| 274 |
-
async def test_str_vs_list_str(self):
|
| 275 |
-
"""Test handling of string vs list[str] type annotations."""
|
| 276 |
-
|
| 277 |
-
def func_with_str_types(str_or_list: str | list[str]) -> str | list[str]:
|
| 278 |
-
return str_or_list
|
| 279 |
-
|
| 280 |
-
tool = Tool.from_function(func_with_str_types)
|
| 281 |
-
|
| 282 |
-
# Test regular string input (should remain a string)
|
| 283 |
-
result = await tool.run({"str_or_list": "hello"})
|
| 284 |
-
assert result[0].text == "hello" # type: ignore[attr-dict]
|
| 285 |
-
|
| 286 |
-
# Test JSON string input (should be parsed as a string)
|
| 287 |
-
result = await tool.run({"str_or_list": '"hello"'})
|
| 288 |
-
assert result[0].text == "hello" # type: ignore[attr-dict]
|
| 289 |
-
|
| 290 |
-
# Test JSON list input (should be parsed as a list)
|
| 291 |
-
result = await tool.run({"str_or_list": '["hello", "world"]'})
|
| 292 |
-
|
| 293 |
-
# The exact formatting might vary, so we just check that it contains the key elements
|
| 294 |
-
text_without_whitespace = result[0].text.replace(" ", "").replace("\n", "") # type: ignore[attr-dict]
|
| 295 |
-
assert "hello" in text_without_whitespace
|
| 296 |
-
assert "world" in text_without_whitespace
|
| 297 |
-
assert "[" in text_without_whitespace
|
| 298 |
-
assert "]" in text_without_whitespace
|
| 299 |
-
|
| 300 |
-
async def test_keep_str_as_str(self):
|
| 301 |
-
"""Test that string arguments are kept as strings when they're not valid JSON"""
|
| 302 |
-
|
| 303 |
-
def func_with_str_types(string: str) -> str:
|
| 304 |
-
return string
|
| 305 |
-
|
| 306 |
-
tool = Tool.from_function(func_with_str_types)
|
| 307 |
-
|
| 308 |
-
# Invalid JSON should remain a string
|
| 309 |
-
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
| 310 |
-
result = await tool.run({"string": invalid_json})
|
| 311 |
-
assert result[0].text == invalid_json # type: ignore[attr-dict]
|
| 312 |
-
|
| 313 |
-
async def test_keep_str_union_as_str(self):
|
| 314 |
-
"""Test that string arguments are kept as strings when parsing would create an invalid value"""
|
| 315 |
-
|
| 316 |
-
def func_with_str_types(
|
| 317 |
-
string: str | dict[int, str] | None,
|
| 318 |
-
) -> str | dict[int, str] | None:
|
| 319 |
-
return string
|
| 320 |
-
|
| 321 |
-
tool = Tool.from_function(func_with_str_types)
|
| 322 |
-
|
| 323 |
-
# Invalid JSON for the union type should remain a string
|
| 324 |
-
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
| 325 |
-
result = await tool.run({"string": invalid_json})
|
| 326 |
-
assert result[0].text == invalid_json # type: ignore[attr-dict]
|
| 327 |
-
|
| 328 |
-
async def test_complex_type_validation(self):
|
| 329 |
-
"""Test that parsed JSON is validated against complex types"""
|
| 330 |
-
|
| 331 |
-
class SomeModel(BaseModel):
|
| 332 |
-
x: int
|
| 333 |
-
y: dict[int, str]
|
| 334 |
-
|
| 335 |
-
def func_with_complex_type(data: SomeModel) -> SomeModel:
|
| 336 |
-
return data
|
| 337 |
-
|
| 338 |
-
tool = Tool.from_function(func_with_complex_type)
|
| 339 |
-
|
| 340 |
-
# Valid JSON for the model
|
| 341 |
-
valid_json = '{"x": 1, "y": {"1": "hello"}}'
|
| 342 |
-
result = await tool.run({"data": valid_json})
|
| 343 |
-
assert '"x": 1' in result[0].text # type: ignore[attr-dict]
|
| 344 |
-
assert '"y": {' in result[0].text # type: ignore[attr-dict]
|
| 345 |
-
assert '"1": "hello"' in result[0].text # type: ignore[attr-dict]
|
| 346 |
-
|
| 347 |
-
# Invalid JSON for the model (y has string keys, not int keys)
|
| 348 |
-
# Should throw a validation error
|
| 349 |
-
invalid_json = '{"x": 1, "y": {"invalid": "hello"}}'
|
| 350 |
-
with pytest.raises(Exception):
|
| 351 |
-
await tool.run({"data": invalid_json})
|
| 352 |
-
|
| 353 |
-
async def test_tool_list_coercion(self):
|
| 354 |
-
"""Test JSON string to collection type coercion."""
|
| 355 |
-
mcp = FastMCP()
|
| 356 |
-
|
| 357 |
-
@mcp.tool
|
| 358 |
-
def process_list(items: list[int]) -> int:
|
| 359 |
-
return sum(items)
|
| 360 |
-
|
| 361 |
-
async with Client(mcp) as client:
|
| 362 |
-
# JSON array string should be coerced to list
|
| 363 |
-
result = await client.call_tool(
|
| 364 |
-
"process_list", {"items": "[1, 2, 3, 4, 5]"}
|
| 365 |
-
)
|
| 366 |
-
assert result[0].text == "15" # type: ignore[attr-dict]
|
| 367 |
-
|
| 368 |
-
async def test_tool_list_coercion_error(self):
|
| 369 |
-
"""Test that a list coercion error is raised if the input is not a valid list."""
|
| 370 |
-
mcp = FastMCP()
|
| 371 |
-
|
| 372 |
-
@mcp.tool
|
| 373 |
-
def process_list(items: list[int]) -> int:
|
| 374 |
-
return sum(items)
|
| 375 |
-
|
| 376 |
-
async with Client(mcp) as client:
|
| 377 |
-
with pytest.raises(
|
| 378 |
-
ToolError,
|
| 379 |
-
match="Error calling tool 'process_list'",
|
| 380 |
-
):
|
| 381 |
-
await client.call_tool("process_list", {"items": "['a', 'b', 3]"})
|
| 382 |
-
|
| 383 |
-
async def test_tool_dict_coercion(self):
|
| 384 |
-
"""Test JSON string to dict type coercion."""
|
| 385 |
-
mcp = FastMCP()
|
| 386 |
-
|
| 387 |
-
@mcp.tool
|
| 388 |
-
def process_dict(data: dict[str, int]) -> int:
|
| 389 |
-
return sum(data.values())
|
| 390 |
-
|
| 391 |
-
async with Client(mcp) as client:
|
| 392 |
-
# JSON object string should be coerced to dict
|
| 393 |
-
result = await client.call_tool(
|
| 394 |
-
"process_dict", {"data": '{"a": 1, "b": "2", "c": 3}'}
|
| 395 |
-
)
|
| 396 |
-
assert result[0].text == "6" # type: ignore[attr-dict]
|
| 397 |
-
|
| 398 |
-
async def test_tool_set_coercion(self):
|
| 399 |
-
"""Test JSON string to set type coercion."""
|
| 400 |
-
mcp = FastMCP()
|
| 401 |
-
|
| 402 |
-
@mcp.tool
|
| 403 |
-
def process_set(items: set[int]) -> int:
|
| 404 |
-
assert isinstance(items, set)
|
| 405 |
-
return sum(items)
|
| 406 |
-
|
| 407 |
-
async with Client(mcp) as client:
|
| 408 |
-
result = await client.call_tool("process_set", {"items": "[1, 2, 3, 4, 5]"})
|
| 409 |
-
assert result[0].text == "15" # type: ignore[attr-dict]
|
| 410 |
-
|
| 411 |
-
async def test_tool_tuple_coercion(self):
|
| 412 |
-
"""Test JSON string to tuple type coercion."""
|
| 413 |
-
mcp = FastMCP()
|
| 414 |
-
|
| 415 |
-
@mcp.tool
|
| 416 |
-
def process_tuple(items: tuple[int, str]) -> int:
|
| 417 |
-
assert isinstance(items, tuple)
|
| 418 |
-
return items[0] + len(items[1])
|
| 419 |
-
|
| 420 |
-
async with Client(mcp) as client:
|
| 421 |
-
result = await client.call_tool("process_tuple", {"items": '["1", "two"]'})
|
| 422 |
-
assert isinstance(result[0], TextContent)
|
| 423 |
-
assert result[0].text == "4" # type: ignore[attr-dict]
|
| 424 |
-
|
| 425 |
-
|
| 426 |
class TestConvertResultToContent:
|
| 427 |
"""Tests for the _convert_to_content helper function."""
|
| 428 |
|
|
|
|
| 10 |
)
|
| 11 |
from pydantic import AnyUrl, BaseModel
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
from fastmcp.tools.tool import Tool, _convert_to_content
|
|
|
|
| 14 |
from fastmcp.utilities.types import Audio, File, Image
|
| 15 |
|
| 16 |
|
|
|
|
| 240 |
assert result[0].text == "Custom serializer: 15"
|
| 241 |
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
class TestConvertResultToContent:
|
| 244 |
"""Tests for the _convert_to_content helper function."""
|
| 245 |
|
tests/tools/test_tool_manager.py
CHANGED
|
@@ -12,7 +12,6 @@ from fastmcp import Context, FastMCP
|
|
| 12 |
from fastmcp.exceptions import NotFoundError, ToolError
|
| 13 |
from fastmcp.tools import FunctionTool, ToolManager
|
| 14 |
from fastmcp.tools.tool import Tool
|
| 15 |
-
from fastmcp.utilities.tests import temporary_settings
|
| 16 |
from fastmcp.utilities.types import Image
|
| 17 |
|
| 18 |
|
|
@@ -434,21 +433,6 @@ class TestCallTools:
|
|
| 434 |
result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
|
| 435 |
assert result[0].text == "6" # type: ignore[attr-defined]
|
| 436 |
|
| 437 |
-
async def test_call_tool_with_list_int_input_legacy_behavior(self):
|
| 438 |
-
"""Legacy behavior -- parse a stringified JSON object"""
|
| 439 |
-
|
| 440 |
-
def sum_vals(vals: list[int]) -> int:
|
| 441 |
-
return sum(vals)
|
| 442 |
-
|
| 443 |
-
manager = ToolManager()
|
| 444 |
-
tool = Tool.from_function(sum_vals)
|
| 445 |
-
manager.add_tool(tool)
|
| 446 |
-
# Try both with plain list and with JSON list
|
| 447 |
-
|
| 448 |
-
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 449 |
-
result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
|
| 450 |
-
assert result[0].text == "6" # type: ignore[attr-defined]
|
| 451 |
-
|
| 452 |
async def test_call_tool_with_list_str_or_str_input(self):
|
| 453 |
def concat_strs(vals: list[str] | str) -> str:
|
| 454 |
return vals if isinstance(vals, str) else "".join(vals)
|
|
@@ -464,23 +448,6 @@ class TestCallTools:
|
|
| 464 |
result = await manager.call_tool("concat_strs", {"vals": "a"})
|
| 465 |
assert result[0].text == "a" # type: ignore[attr-defined]
|
| 466 |
|
| 467 |
-
async def test_call_tool_with_list_str_or_str_input_legacy_behavior(self):
|
| 468 |
-
"""Legacy behavior -- parse a stringified JSON object"""
|
| 469 |
-
|
| 470 |
-
def concat_strs(vals: list[str] | str) -> str:
|
| 471 |
-
return vals if isinstance(vals, str) else "".join(vals)
|
| 472 |
-
|
| 473 |
-
manager = ToolManager()
|
| 474 |
-
tool = Tool.from_function(concat_strs)
|
| 475 |
-
manager.add_tool(tool)
|
| 476 |
-
|
| 477 |
-
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 478 |
-
result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
|
| 479 |
-
assert result[0].text == "abc" # type: ignore[attr-defined]
|
| 480 |
-
|
| 481 |
-
result = await manager.call_tool("concat_strs", {"vals": '"a"'})
|
| 482 |
-
assert result[0].text == "a" # type: ignore[attr-defined]
|
| 483 |
-
|
| 484 |
async def test_call_tool_with_complex_model(self):
|
| 485 |
class MyShrimpTank(BaseModel):
|
| 486 |
class Shrimp(BaseModel):
|
|
|
|
| 12 |
from fastmcp.exceptions import NotFoundError, ToolError
|
| 13 |
from fastmcp.tools import FunctionTool, ToolManager
|
| 14 |
from fastmcp.tools.tool import Tool
|
|
|
|
| 15 |
from fastmcp.utilities.types import Image
|
| 16 |
|
| 17 |
|
|
|
|
| 433 |
result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
|
| 434 |
assert result[0].text == "6" # type: ignore[attr-defined]
|
| 435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
async def test_call_tool_with_list_str_or_str_input(self):
|
| 437 |
def concat_strs(vals: list[str] | str) -> str:
|
| 438 |
return vals if isinstance(vals, str) else "".join(vals)
|
|
|
|
| 448 |
result = await manager.call_tool("concat_strs", {"vals": "a"})
|
| 449 |
assert result[0].text == "a" # type: ignore[attr-defined]
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
async def test_call_tool_with_complex_model(self):
|
| 452 |
class MyShrimpTank(BaseModel):
|
| 453 |
class Shrimp(BaseModel):
|