Repository: jlowin/fastmcp (python)

Issue:
api endpoints returning lists of items cause error in openapi
### Description

Given there's an endpoint in openapi spec consisting of a list as root.
When the endpoint gets called and returns back the list
The response is not converted to TextContent and therefor throws an error

### Example Code

```Python

```

### Version Information

```Text
Looks like the issue is fixed by adding list here:
https://github.com/jlowin/fastmcp/blob/ee68d1429d4fc3cf10271e3b0a4abb1404db3a09/src/fastmcp/server/openapi.py#L594-L599
```

### Additional Context

_No response_

Pull request description:
Ensure openapi tool responses are properly converted
Closes #275, supersedes #277 

If an openapi tool returned a list, it was not properly converted to an MCP response. This ensures _convert_to_content is always called.

Patch:
diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py
index 65e3781..ce26dd8 100644
--- a/src/fastmcp/server/openapi.py
+++ b/src/fastmcp/server/openapi.py
@@ -10,12 +10,12 @@ from re import Pattern
 from typing import TYPE_CHECKING, Any, Literal
 
 import httpx
-from mcp.types import TextContent
+from mcp.types import EmbeddedResource, ImageContent, TextContent
 from pydantic.networks import AnyUrl
 
 from fastmcp.resources import Resource, ResourceTemplate
 from fastmcp.server.server import FastMCP
-from fastmcp.tools.tool import Tool
+from fastmcp.tools.tool import Tool, _convert_to_content
 from fastmcp.utilities import openapi
 from fastmcp.utilities.func_metadata import func_metadata
 from fastmcp.utilities.logging import get_logger
@@ -239,9 +239,14 @@ class OpenAPITool(Tool):
             # Handle request errors (connection, timeout, etc.)
             raise ValueError(f"Request error: {str(e)}")
 
-    async def run(self, arguments: dict[str, Any], context: Any = None) -> Any:
+    async def run(
+        self,
+        arguments: dict[str, Any],
+        context: Context[ServerSessionT, LifespanContextT] | None = None,
+    ) -> list[TextContent | ImageContent | EmbeddedResource]:
         """Run the tool with arguments and optional context."""
-        return await self._execute_request(**arguments, context=context)
+        response = await self._execute_request(**arguments, context=context)
+        return _convert_to_content(response)
 
 
 class OpenAPIResource(Resource):
@@ -605,13 +610,4 @@ class FastMCPOpenAPI(FastMCP):
 
         context = self.get_context()
         result = await self._tool_manager.call_tool(name, arguments, context=context)
-
-        # For other tools, ensure the response is wrapped in TextContent
-        if isinstance(result, dict | str):
-            if isinstance(result, dict):
-                result_text = json.dumps(result)
-            else:
-                result_text = result
-            return [TextContent(text=result_text, type="text")]
-
         return result
diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py
index e7cd35d..ca16e52 100644
--- a/src/fastmcp/server/proxy.py
+++ b/src/fastmcp/server/proxy.py
@@ -61,7 +61,7 @@ class ProxyTool(Tool):
         self,
         arguments: dict[str, Any],
         context: Context[ServerSessionT, LifespanContextT] | None = None,
-    ) -> Any:
+    ) -> list[TextContent | ImageContent | EmbeddedResource]:
         # the client context manager will swallow any exceptions inside a TaskGroup
         # so we return the raw result and raise an exception ourselves
         async with self._client:


Test patch:
diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py
index 49cc6f8..8ef8c05 100644
--- a/tests/server/test_openapi.py
+++ b/tests/server/test_openapi.py
@@ -19,6 +19,8 @@ from fastmcp.server.openapi import (
     OpenAPIResource,
     OpenAPIResourceTemplate,
     OpenAPITool,
+    RouteMap,
+    RouteType,
 )
 
 
@@ -268,6 +270,32 @@ class TestTools:
             user = json.loads(response_text)
         assert user == expected_data
 
+    async def test_call_tool_return_list(
+        self,
+        fastapi_app: FastAPI,
+        api_client: httpx.AsyncClient,
+        users_db: dict[int, User],
+    ):
+        """
+        The tool created by the OpenAPI server should return a list of content.
+        """
+        openapi_spec = fastapi_app.openapi()
+        mcp_server = FastMCPOpenAPI(
+            openapi_spec=openapi_spec,
+            client=api_client,
+            route_maps=[
+                RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
+            ],
+        )
+        async with Client(mcp_server) as client:
+            tool_response = await client.call_tool("get_users_users_get", {})
+            assert isinstance(tool_response, list)
+            assert isinstance(tool_response[0], TextContent)
+            assert json.loads(tool_response[0].text) == [
+                user.model_dump()
+                for user in sorted(users_db.values(), key=lambda x: x.id)
+            ]
+
 
 class TestResources:
     async def test_list_resources(self, fastmcp_openapi_server: FastMCPOpenAPI):