diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index bd159663c..1477fab53 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -350,9 +350,7 @@ class FunctionPrompt(Prompt): ) ) else: - content = pydantic_core.to_json( - msg, fallback=str, indent=2 - ).decode() + content = pydantic_core.to_json(msg, fallback=str).decode() messages.append( PromptMessage( role="user", diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index a88174b8a..b16d0dd89 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -192,4 +192,4 @@ class FunctionResource(Resource): elif isinstance(result, str): return result else: - return pydantic_core.to_json(result, fallback=str, indent=2).decode() + return pydantic_core.to_json(result, fallback=str).decode() diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 6cca87919..e6195a2d3 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -46,7 +46,7 @@ class _UnserializableType: def default_serializer(data: Any) -> str: - return pydantic_core.to_json(data, fallback=str, indent=2).decode() + return pydantic_core.to_json(data, fallback=str).decode() class ToolResult: @@ -434,6 +434,7 @@ def _convert_to_content( _process_as_single_item: bool = False, ) -> list[ContentBlock]: """Convert a result to a sequence of content objects.""" + if result is None: return [] @@ -467,7 +468,7 @@ def _convert_to_content( if other_content: other_content = _convert_to_content( - other_content[0] if len(other_content) == 1 else other_content, + other_content, serializer=serializer, _process_as_single_item=True, ) diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index 0f965c275..88ae17495 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -9,7 +9,7 @@ from typing import Any, Literal from mcp.types import ToolAnnotations from pydantic import ConfigDict -from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult +from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content from fastmcp.utilities.logging import get_logger from fastmcp.utilities.types import NotSet, NotSetT, get_cached_typeadapter @@ -233,7 +233,6 @@ class TransformedTool(Tool): Returns: ToolResult object containing content and optional structured output. """ - from fastmcp.tools.tool import _convert_to_content # Fill in missing arguments with schema defaults to ensure # ArgTransform defaults take precedence over function defaults @@ -274,7 +273,6 @@ class TransformedTool(Tool): if isinstance(result, ToolResult): if self.output_schema is None: # Check if this is from a custom function that returns ToolResult - import inspect return_annotation = inspect.signature(self.fn).return_annotation if return_annotation is ToolResult: @@ -298,7 +296,6 @@ class TransformedTool(Tool): return result # Otherwise convert to content and create ToolResult with proper structured content - from fastmcp.tools.tool import _convert_to_content unstructured_result = _convert_to_content( result, serializer=self.serializer @@ -433,8 +430,6 @@ class TransformedTool(Tool): final_output_schema = parsed_fn.output_schema if final_output_schema is None: # Check if function returns ToolResult - if so, don't fall back to parent - import inspect - return_annotation = inspect.signature( transform_fn ).return_annotation diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 499a3f256..c3bd40261 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -536,9 +536,9 @@ async def test_resource_template(fastmcp_server): # Check the content matches what we expect for the provided user_id content_str = str(result[0]) - assert '"id": "123"' in content_str - assert '"name": "User 123"' in content_str - assert '"active": true' in content_str + assert '"id":"123"' in content_str + assert '"name":"User 123"' in content_str + assert '"active":true' in content_str async def test_list_resource_templates_mcp(fastmcp_server): @@ -595,7 +595,7 @@ async def test_template_access_via_client(fastmcp_server): uri = cast(AnyUrl, "data://user/456") result = await client.read_resource(uri) content_str = str(result[0]) - assert '"id": "456"' in content_str + assert '"id":"456"' in content_str async def test_tagged_resource_metadata(tagged_resources_server): @@ -635,8 +635,8 @@ async def test_tagged_template_functionality(tagged_resources_server): uri = cast(AnyUrl, "template://123") result = await client.read_resource(uri) content_str = str(result[0]) - assert '"id": "123"' in content_str - assert '"type": "template_data"' in content_str + assert '"id":"123"' in content_str + assert '"type":"template_data"' in content_str class TestErrorHandling: diff --git a/tests/resources/test_function_resources.py b/tests/resources/test_function_resources.py index af69896be..c6c0696d2 100644 --- a/tests/resources/test_function_resources.py +++ b/tests/resources/test_function_resources.py @@ -67,7 +67,7 @@ class TestFunctionResource: ) content = await resource.read() assert isinstance(content, str) - assert '"key": "value"' in content + assert '"key":"value"' in content async def test_error_handling(self): """Test error handling in FunctionResource.""" @@ -95,7 +95,7 @@ class TestFunctionResource: fn=lambda: MyModel(name="test"), ) content = await resource.read() - assert content == '{\n "name": "test"\n}' + assert content == '{"name":"test"}' async def test_custom_type_conversion(self): """Test handling of custom types.""" diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 8d905919e..986fb275a 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -170,7 +170,7 @@ class TestTools: async def test_tool_returns_list(self, tool_server: FastMCP): async with Client(tool_server) as client: result = await client.call_tool("list_tool", {}) - assert result.content[0].text == '[\n "x",\n 2\n]' # type: ignore[attr-defined] + assert result.content[0].text == '["x",2]' # type: ignore[attr-defined] assert result.data == ["x", 2] async def test_file_text_tool(self, tool_server: FastMCP): diff --git a/tests/test_examples.py b/tests/test_examples.py index 0edeee96e..62fe38f28 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -25,7 +25,7 @@ async def test_complex_inputs(): "name_shrimp", {"tank": tank, "extra_names": ["charlie"]} ) assert len(result.content) == 1 - assert result.content[0].text == '[\n "bob",\n "alice",\n "charlie"\n]' # type: ignore[attr-defined] + assert result.content[0].text == '["bob","alice","charlie"]' # type: ignore[attr-defined] async def test_desktop(monkeypatch): diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index afe86d2ff..93410f6be 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -553,7 +553,7 @@ class TestToolFromFunctionOutputSchema: # Dict objects automatically become structured content even without schema assert result.structured_content == {"message": "Hello, world!"} assert len(result.content) == 1 - assert result.content[0].text == '{\n "message": "Hello, world!"\n}' # type: ignore[attr-defined] + assert result.content[0].text == '{"message":"Hello, world!"}' # type: ignore[attr-defined] async def test_output_schema_none_disables_structured_content(self): """Test that output_schema=None explicitly disables structured content.""" @@ -607,7 +607,7 @@ class TestToolFromFunctionOutputSchema: result = await tool.run({}) # Dict result with object schema is used directly assert result.structured_content == {"value": 42} - assert result.content[0].text == '{\n "value": 42\n}' # type: ignore[attr-defined] + assert result.content[0].text == '{"value":42}' # type: ignore[attr-defined] async def test_explicit_object_schema_with_non_dict_return_fails(self): """Test that explicit object schemas fail when function returns non-dict.""" @@ -859,7 +859,7 @@ class TestConvertResultToContent: assert isinstance(result, list) assert len(result) == 1 assert isinstance(result[0], TextContent) - assert result[0].text == '{\n "a": 1,\n "b": 2\n}' + assert result[0].text == '{"a":1,"b":2}' def test_list_of_basic_types(self): """Test that a list of basic types is converted to a single TextContent.""" @@ -867,7 +867,7 @@ class TestConvertResultToContent: assert isinstance(result, list) assert len(result) == 1 assert isinstance(result[0], TextContent) - assert result[0].text == '[\n 1,\n "two",\n {\n "c": 3\n }\n]' + assert result[0].text == '[1,"two",{"c":3}]' def test_list_of_mcp_types(self): """Test that a list of MCP types is returned as a list of those types.""" @@ -898,7 +898,7 @@ class TestConvertResultToContent: assert image_content_count == 1 text_item = next(item for item in result if isinstance(item, TextContent)) - assert text_item.text == '{\n "a": 1\n}' + assert text_item.text == '[{"a":1}]' image_item = next(item for item in result if isinstance(item, ImageContent)) assert image_item.data == "ZmFrZWltYWdlZGF0YQ==" @@ -920,7 +920,7 @@ class TestConvertResultToContent: assert image_content_count == 1 text_item = next(item for item in result if isinstance(item, TextContent)) - assert text_item.text == '[\n {\n "a": 1\n },\n {\n "b": 2\n }\n]' + assert text_item.text == '[[{"a":1},{"b":2}]]' image_item = next(item for item in result if isinstance(item, ImageContent)) assert image_item.data == "ZmFrZWltYWdlZGF0YQ==" @@ -942,7 +942,7 @@ class TestConvertResultToContent: assert audio_content_count == 1 text_item = next(item for item in result if isinstance(item, TextContent)) - assert text_item.text == '{\n "a": 1\n}' + assert text_item.text == '[{"a":1}]' audio_item = next(item for item in result if isinstance(item, AudioContent)) assert audio_item.data == "ZmFrZWF1ZGlvZGF0YQ==" @@ -967,7 +967,7 @@ class TestConvertResultToContent: assert embedded_content_count == 1 text_item = next(item for item in result if isinstance(item, TextContent)) - assert text_item.text == '{\n "a": 1\n}' + assert text_item.text == '[{"a":1}]' embedded_item = next( item @@ -1031,7 +1031,7 @@ class TestConvertResultToContent: assert isinstance(result, list) assert len(result) == 1 assert isinstance(result[0], TextContent) - assert result[0].text == '[\n 1,\n "two",\n {\n "c": 3\n }\n]' + assert result[0].text == '[1,"two",{"c":3}]' content1 = TextContent(type="text", text="hello") result = _convert_to_content([1, content1], _process_as_single_item=True) @@ -1044,6 +1044,30 @@ class TestConvertResultToContent: {"type": "text", "text": "hello", "annotations": None, "_meta": None}, ] + def test_single_element_list_preserves_structure(self): + """Test that single-element lists preserve their list structure.""" + + # Test with a single integer + result = _convert_to_content([1]) + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], TextContent) + assert result[0].text == "[1]" # Should be "[1]", not "1" + + # Test with a single string + result = _convert_to_content(["hello"]) + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], TextContent) + assert result[0].text == '["hello"]' # Should be ["hello"], not "hello" + + # Test with a single dict + result = _convert_to_content([{"a": 1}]) + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], TextContent) + assert result[0].text == '[{"a":1}]' # Should be wrapped in a list + class TestAutomaticStructuredContent: """Tests for automatic structured content generation based on return types.""" diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index 1e3611548..42ef6038e 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -489,7 +489,7 @@ class TestCallTools: }, ) - assert result.content[0].text == '[\n "rex",\n "gertrude"\n]' # type: ignore[attr-defined] + assert result.content[0].text == '["rex","gertrude"]' # type: ignore[attr-defined] assert result.structured_content == {"result": ["rex", "gertrude"]} async def test_call_tool_with_custom_serializer(self): diff --git a/tests/tools/test_tool_transform.py b/tests/tools/test_tool_transform.py index e0460d9b6..85fced3f9 100644 --- a/tests/tools/test_tool_transform.py +++ b/tests/tools/test_tool_transform.py @@ -1157,7 +1157,7 @@ class TestTransformToolOutputSchema: result = await new_tool.run({"x": 3}) # Should wrap string result - assert result.structured_content == {"result": 'Custom: {\n "value": 3\n}'} + assert result.structured_content == {"result": 'Custom: {"value":3}'} def test_transform_custom_function_fallback_to_parent(self, base_string_tool): """Test that custom function without output annotation falls back to parent."""