From 39e62bc11cb1c8dabb27485b688aa733b76a28cc Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:09:33 -0400 Subject: [PATCH] Ensure openapi tool responses are properly converted --- src/fastmcp/server/openapi.py | 22 +++++++++------------- src/fastmcp/server/proxy.py | 2 +- tests/server/test_openapi.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 65e3781b8..ce26dd879 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 e7cd35d68..ca16e5246 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: diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 49cc6f8f0..8ef8c0560 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):