Spaces:
Running
Running
Merge pull request #283 from jlowin/openapi-response
Browse files- src/fastmcp/server/openapi.py +9 -13
- src/fastmcp/server/proxy.py +1 -1
- tests/server/test_openapi.py +28 -0
src/fastmcp/server/openapi.py
CHANGED
|
@@ -10,12 +10,12 @@ from re import Pattern
|
|
| 10 |
from typing import TYPE_CHECKING, Any, Literal
|
| 11 |
|
| 12 |
import httpx
|
| 13 |
-
from mcp.types import TextContent
|
| 14 |
from pydantic.networks import AnyUrl
|
| 15 |
|
| 16 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 17 |
from fastmcp.server.server import FastMCP
|
| 18 |
-
from fastmcp.tools.tool import Tool
|
| 19 |
from fastmcp.utilities import openapi
|
| 20 |
from fastmcp.utilities.func_metadata import func_metadata
|
| 21 |
from fastmcp.utilities.logging import get_logger
|
|
@@ -239,9 +239,14 @@ class OpenAPITool(Tool):
|
|
| 239 |
# Handle request errors (connection, timeout, etc.)
|
| 240 |
raise ValueError(f"Request error: {str(e)}")
|
| 241 |
|
| 242 |
-
async def run(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
"""Run the tool with arguments and optional context."""
|
| 244 |
-
|
|
|
|
| 245 |
|
| 246 |
|
| 247 |
class OpenAPIResource(Resource):
|
|
@@ -605,13 +610,4 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 605 |
|
| 606 |
context = self.get_context()
|
| 607 |
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
| 608 |
-
|
| 609 |
-
# For other tools, ensure the response is wrapped in TextContent
|
| 610 |
-
if isinstance(result, dict | str):
|
| 611 |
-
if isinstance(result, dict):
|
| 612 |
-
result_text = json.dumps(result)
|
| 613 |
-
else:
|
| 614 |
-
result_text = result
|
| 615 |
-
return [TextContent(text=result_text, type="text")]
|
| 616 |
-
|
| 617 |
return result
|
|
|
|
| 10 |
from typing import TYPE_CHECKING, Any, Literal
|
| 11 |
|
| 12 |
import httpx
|
| 13 |
+
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
| 14 |
from pydantic.networks import AnyUrl
|
| 15 |
|
| 16 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 17 |
from fastmcp.server.server import FastMCP
|
| 18 |
+
from fastmcp.tools.tool import Tool, _convert_to_content
|
| 19 |
from fastmcp.utilities import openapi
|
| 20 |
from fastmcp.utilities.func_metadata import func_metadata
|
| 21 |
from fastmcp.utilities.logging import get_logger
|
|
|
|
| 239 |
# Handle request errors (connection, timeout, etc.)
|
| 240 |
raise ValueError(f"Request error: {str(e)}")
|
| 241 |
|
| 242 |
+
async def run(
|
| 243 |
+
self,
|
| 244 |
+
arguments: dict[str, Any],
|
| 245 |
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
| 246 |
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
| 247 |
"""Run the tool with arguments and optional context."""
|
| 248 |
+
response = await self._execute_request(**arguments, context=context)
|
| 249 |
+
return _convert_to_content(response)
|
| 250 |
|
| 251 |
|
| 252 |
class OpenAPIResource(Resource):
|
|
|
|
| 610 |
|
| 611 |
context = self.get_context()
|
| 612 |
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
return result
|
src/fastmcp/server/proxy.py
CHANGED
|
@@ -61,7 +61,7 @@ class ProxyTool(Tool):
|
|
| 61 |
self,
|
| 62 |
arguments: dict[str, Any],
|
| 63 |
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
| 64 |
-
) ->
|
| 65 |
# the client context manager will swallow any exceptions inside a TaskGroup
|
| 66 |
# so we return the raw result and raise an exception ourselves
|
| 67 |
async with self._client:
|
|
|
|
| 61 |
self,
|
| 62 |
arguments: dict[str, Any],
|
| 63 |
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
| 64 |
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
| 65 |
# the client context manager will swallow any exceptions inside a TaskGroup
|
| 66 |
# so we return the raw result and raise an exception ourselves
|
| 67 |
async with self._client:
|
tests/server/test_openapi.py
CHANGED
|
@@ -19,6 +19,8 @@ from fastmcp.server.openapi import (
|
|
| 19 |
OpenAPIResource,
|
| 20 |
OpenAPIResourceTemplate,
|
| 21 |
OpenAPITool,
|
|
|
|
|
|
|
| 22 |
)
|
| 23 |
|
| 24 |
|
|
@@ -268,6 +270,32 @@ class TestTools:
|
|
| 268 |
user = json.loads(response_text)
|
| 269 |
assert user == expected_data
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
class TestResources:
|
| 273 |
async def test_list_resources(self, fastmcp_openapi_server: FastMCPOpenAPI):
|
|
|
|
| 19 |
OpenAPIResource,
|
| 20 |
OpenAPIResourceTemplate,
|
| 21 |
OpenAPITool,
|
| 22 |
+
RouteMap,
|
| 23 |
+
RouteType,
|
| 24 |
)
|
| 25 |
|
| 26 |
|
|
|
|
| 270 |
user = json.loads(response_text)
|
| 271 |
assert user == expected_data
|
| 272 |
|
| 273 |
+
async def test_call_tool_return_list(
|
| 274 |
+
self,
|
| 275 |
+
fastapi_app: FastAPI,
|
| 276 |
+
api_client: httpx.AsyncClient,
|
| 277 |
+
users_db: dict[int, User],
|
| 278 |
+
):
|
| 279 |
+
"""
|
| 280 |
+
The tool created by the OpenAPI server should return a list of content.
|
| 281 |
+
"""
|
| 282 |
+
openapi_spec = fastapi_app.openapi()
|
| 283 |
+
mcp_server = FastMCPOpenAPI(
|
| 284 |
+
openapi_spec=openapi_spec,
|
| 285 |
+
client=api_client,
|
| 286 |
+
route_maps=[
|
| 287 |
+
RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
|
| 288 |
+
],
|
| 289 |
+
)
|
| 290 |
+
async with Client(mcp_server) as client:
|
| 291 |
+
tool_response = await client.call_tool("get_users_users_get", {})
|
| 292 |
+
assert isinstance(tool_response, list)
|
| 293 |
+
assert isinstance(tool_response[0], TextContent)
|
| 294 |
+
assert json.loads(tool_response[0].text) == [
|
| 295 |
+
user.model_dump()
|
| 296 |
+
for user in sorted(users_db.values(), key=lambda x: x.id)
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
|
| 300 |
class TestResources:
|
| 301 |
async def test_list_resources(self, fastmcp_openapi_server: FastMCPOpenAPI):
|