Ensure openapi tool responses are properly converted

This commit is contained in:
Jeremiah Lowin 2025-04-30 10:09:33 -04:00
commit 39e62bc11c
3 changed files with 38 additions and 14 deletions

View file

@ -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

View file

@ -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:

View file

@ -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):