diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
index 64fba8da3d..568d3d8d34 100644
--- a/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
@@ -27,7 +27,7 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Execute the HTTP request using RequestDirector.
-### `OpenAPIResource`
+### `OpenAPIResource`
Resource implementation for OpenAPI endpoints.
@@ -35,7 +35,7 @@ Resource implementation for OpenAPI endpoints.
**Methods:**
-#### `read`
+#### `read`
```python
read(self) -> ResourceResult
@@ -44,7 +44,7 @@ read(self) -> ResourceResult
Fetch the resource data by making an HTTP request.
-### `OpenAPIResourceTemplate`
+### `OpenAPIResourceTemplate`
Resource template implementation for OpenAPI endpoints.
@@ -52,7 +52,7 @@ Resource template implementation for OpenAPI endpoints.
**Methods:**
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource
diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
index 6892d11746..69c2b0dae8 100644
--- a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
@@ -21,7 +21,7 @@ spec. Each component makes HTTP calls to the described API endpoints.
**Methods:**
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> AsyncIterator[None]
@@ -30,7 +30,7 @@ lifespan(self) -> AsyncIterator[None]
Manage the lifecycle of the auto-created httpx client.
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index 56bfd76966..80b45257b0 100644
--- a/docs/python-sdk/fastmcp-server-server.mdx
+++ b/docs/python-sdk/fastmcp-server-server.mdx
@@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.
-### `create_proxy`
+### `create_proxy`
```python
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@@ -823,7 +823,7 @@ objects are imported with their original names.
#### `from_openapi`
```python
-from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
+from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self
```
Create a FastMCP server from an OpenAPI specification.
@@ -839,13 +839,17 @@ server URL from the OpenAPI spec with a 30-second timeout.
- `mcp_component_fn`: Optional callable for component customization
- `mcp_names`: Optional dictionary mapping operationId to component names
- `tags`: Optional set of tags to add to all components
+- `validate_output`: If True (default), tools use the output schema
+extracted from the OpenAPI spec for response validation. If
+False, a permissive schema is used instead, allowing any
+response structure while still returning structured JSON.
- `**settings`: Additional settings passed to FastMCP
**Returns:**
- A FastMCP server with an OpenAPIProvider attached.
-#### `from_fastapi`
+#### `from_fastapi`
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
@@ -869,7 +873,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.
-#### `as_proxy`
+#### `as_proxy`
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@@ -887,7 +891,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
-#### `generate_name`
+#### `generate_name`
```python
generate_name(cls, name: str | None = None) -> str
diff --git a/src/fastmcp/server/providers/openapi/components.py b/src/fastmcp/server/providers/openapi/components.py
index aa6ef80828..6b942d22e2 100644
--- a/src/fastmcp/server/providers/openapi/components.py
+++ b/src/fastmcp/server/providers/openapi/components.py
@@ -201,6 +201,12 @@ async def run(self, arguments: dict[str, Any]) -> ToolResult:
else:
structured_output = result
+ # Structured content must be a dict for the MCP protocol.
+ # Wrap non-dict values that slipped through (e.g. a backend
+ # returning an array when the schema declared an object).
+ if not isinstance(structured_output, dict):
+ structured_output = {"result": structured_output}
+
return ToolResult(structured_content=structured_output)
except json.JSONDecodeError:
return ToolResult(content=response.text)
diff --git a/src/fastmcp/server/providers/openapi/provider.py b/src/fastmcp/server/providers/openapi/provider.py
index ac79af400d..68979c0c1d 100644
--- a/src/fastmcp/server/providers/openapi/provider.py
+++ b/src/fastmcp/server/providers/openapi/provider.py
@@ -79,6 +79,7 @@ def __init__(
mcp_component_fn: ComponentFn | None = None,
mcp_names: dict[str, str] | None = None,
tags: set[str] | None = None,
+ validate_output: bool = True,
):
"""Initialize provider by parsing OpenAPI spec and creating components.
@@ -93,6 +94,10 @@ def __init__(
mcp_component_fn: Optional callable for component customization
mcp_names: Optional dictionary mapping operationId to component names
tags: Optional set of tags to add to all components
+ validate_output: If True (default), tools use the output schema
+ extracted from the OpenAPI spec for response validation. If
+ False, a permissive schema is used instead, allowing any
+ response structure while still returning structured JSON.
"""
super().__init__()
@@ -101,6 +106,7 @@ def __init__(
client = self._create_default_client(openapi_spec)
self._client = client
self._mcp_component_fn = mcp_component_fn
+ self._validate_output = validate_output
# Keep track of names to detect collisions
self._used_names: dict[str, Counter[str]] = {
@@ -232,6 +238,17 @@ def _create_openapi_tool(
route.openapi_version,
)
+ if not self._validate_output and output_schema is not None:
+ # Use a permissive schema that accepts any object, preserving
+ # the wrap-result flag so non-object responses still get wrapped
+ permissive: dict[str, Any] = {
+ "type": "object",
+ "additionalProperties": True,
+ }
+ if output_schema.get("x-fastmcp-wrap-result"):
+ permissive["x-fastmcp-wrap-result"] = True
+ output_schema = permissive
+
tool_name = self._get_unique_name(name, "tool")
base_description = (
route.description
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 7dc7478216..35d717e8cb 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -2029,6 +2029,7 @@ def from_openapi(
mcp_component_fn: OpenAPIComponentFn | None = None,
mcp_names: dict[str, str] | None = None,
tags: set[str] | None = None,
+ validate_output: bool = True,
**settings: Any,
) -> Self:
"""
@@ -2045,6 +2046,10 @@ def from_openapi(
mcp_component_fn: Optional callable for component customization
mcp_names: Optional dictionary mapping operationId to component names
tags: Optional set of tags to add to all components
+ validate_output: If True (default), tools use the output schema
+ extracted from the OpenAPI spec for response validation. If
+ False, a permissive schema is used instead, allowing any
+ response structure while still returning structured JSON.
**settings: Additional settings passed to FastMCP
Returns:
@@ -2060,6 +2065,7 @@ def from_openapi(
mcp_component_fn=mcp_component_fn,
mcp_names=mcp_names,
tags=tags,
+ validate_output=validate_output,
)
return cls(name=name, providers=[provider], **settings)
diff --git a/tests/server/providers/openapi/test_openapi_features.py b/tests/server/providers/openapi/test_openapi_features.py
index f55a1e038b..ef4aeb002b 100644
--- a/tests/server/providers/openapi/test_openapi_features.py
+++ b/tests/server/providers/openapi/test_openapi_features.py
@@ -1,7 +1,10 @@
"""Tests for OpenAPI feature support in OpenAPIProvider."""
+from unittest.mock import AsyncMock, Mock
+
import httpx
import pytest
+from httpx import Response
from fastmcp import FastMCP
from fastmcp.client import Client
@@ -769,3 +772,213 @@ async def test_resource_mime_type_without_schema(self):
resources = await mcp_client.list_resources()
assert len(resources) == 1
assert resources[0].mimeType == "text/plain"
+
+
+class TestValidateOutput:
+ """Tests for the validate_output option on OpenAPIProvider."""
+
+ @pytest.fixture
+ def spec_with_output_schema(self):
+ return {
+ "openapi": "3.0.0",
+ "info": {"title": "Test API", "version": "1.0.0"},
+ "servers": [{"url": "https://api.example.com"}],
+ "paths": {
+ "/users/{id}": {
+ "get": {
+ "operationId": "get_user",
+ "summary": "Get a user",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": True,
+ "schema": {"type": "integer"},
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "integer"},
+ "name": {"type": "string"},
+ "email": {"type": "string"},
+ },
+ "required": ["id", "name"],
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/items": {
+ "get": {
+ "operationId": "list_items",
+ "summary": "List items",
+ "responses": {
+ "200": {
+ "description": "An array of items",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"}
+ },
+ },
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ },
+ }
+
+ async def test_validate_output_true_preserves_extracted_schema(
+ self, spec_with_output_schema
+ ):
+ """Default validate_output=True uses the real extracted schema."""
+ async with httpx.AsyncClient(base_url="https://api.example.com") as client:
+ provider = OpenAPIProvider(
+ openapi_spec=spec_with_output_schema,
+ client=client,
+ )
+
+ tool = provider._tools["get_user"]
+ assert tool.output_schema is not None
+ assert tool.output_schema.get("type") == "object"
+ assert "properties" in tool.output_schema
+ assert "id" in tool.output_schema["properties"]
+
+ async def test_validate_output_false_uses_permissive_schema(
+ self, spec_with_output_schema
+ ):
+ """validate_output=False replaces the schema with a permissive one."""
+ async with httpx.AsyncClient(base_url="https://api.example.com") as client:
+ provider = OpenAPIProvider(
+ openapi_spec=spec_with_output_schema,
+ client=client,
+ validate_output=False,
+ )
+
+ tool = provider._tools["get_user"]
+ assert tool.output_schema is not None
+ assert tool.output_schema == {
+ "type": "object",
+ "additionalProperties": True,
+ }
+
+ async def test_validate_output_false_preserves_wrap_result_flag(
+ self, spec_with_output_schema
+ ):
+ """validate_output=False preserves x-fastmcp-wrap-result for array responses."""
+ async with httpx.AsyncClient(base_url="https://api.example.com") as client:
+ provider = OpenAPIProvider(
+ openapi_spec=spec_with_output_schema,
+ client=client,
+ validate_output=False,
+ )
+
+ # The list_items endpoint returns an array, so the extracted schema
+ # would have had x-fastmcp-wrap-result=True
+ tool = provider._tools["list_items"]
+ assert tool.output_schema is not None
+ assert tool.output_schema.get("x-fastmcp-wrap-result") is True
+ assert tool.output_schema.get("additionalProperties") is True
+
+ async def test_validate_output_false_allows_nonconforming_response(
+ self, spec_with_output_schema
+ ):
+ """With validate_output=False, responses that don't match the spec succeed."""
+ mock_client = Mock(spec=httpx.AsyncClient)
+ mock_client.base_url = "https://api.example.com"
+ mock_client.headers = None
+
+ # Return extra fields not in the schema
+ mock_response = Mock(spec=Response)
+ mock_response.status_code = 200
+ mock_response.json.return_value = {
+ "id": 1,
+ "name": "Alice",
+ "email": "alice@example.com",
+ "unexpected_field": "surprise",
+ "nested": {"deep": True},
+ }
+ mock_response.raise_for_status = Mock()
+ mock_client.send = AsyncMock(return_value=mock_response)
+
+ provider = OpenAPIProvider(
+ openapi_spec=spec_with_output_schema,
+ client=mock_client,
+ validate_output=False,
+ )
+ mcp = FastMCP("Test")
+ mcp.add_provider(provider)
+
+ async with Client(mcp) as mcp_client:
+ result = await mcp_client.call_tool("get_user", {"id": 1})
+ assert result is not None
+ # Structured content should have the full response including extra fields
+ assert result.structured_content is not None
+ assert result.structured_content["unexpected_field"] == "surprise"
+
+ async def test_validate_output_false_wraps_non_dict_response(
+ self, spec_with_output_schema
+ ):
+ """Non-dict responses are wrapped even when schema says object and validate_output=False."""
+ mock_client = Mock(spec=httpx.AsyncClient)
+ mock_client.base_url = "https://api.example.com"
+ mock_client.headers = None
+
+ # Backend returns an array even though schema says object
+ mock_response = Mock(spec=Response)
+ mock_response.status_code = 200
+ mock_response.json.return_value = [{"id": 1}, {"id": 2}]
+ mock_response.raise_for_status = Mock()
+ mock_client.send = AsyncMock(return_value=mock_response)
+
+ provider = OpenAPIProvider(
+ openapi_spec=spec_with_output_schema,
+ client=mock_client,
+ validate_output=False,
+ )
+ mcp = FastMCP("Test")
+ mcp.add_provider(provider)
+
+ async with Client(mcp) as mcp_client:
+ result = await mcp_client.call_tool("get_user", {"id": 1})
+ assert result is not None
+ # Non-dict should be wrapped so structured_content is always a dict
+ assert result.structured_content is not None
+ assert isinstance(result.structured_content, dict)
+ assert result.structured_content["result"] == [{"id": 1}, {"id": 2}]
+
+ async def test_from_openapi_threads_validate_output(self, spec_with_output_schema):
+ """FastMCP.from_openapi() correctly passes validate_output to the provider."""
+ mock_client = Mock(spec=httpx.AsyncClient)
+ mock_client.base_url = "https://api.example.com"
+ mock_client.headers = None
+
+ server = FastMCP.from_openapi(
+ openapi_spec=spec_with_output_schema,
+ client=mock_client,
+ validate_output=False,
+ )
+
+ async with Client(server) as mcp_client:
+ tools = await mcp_client.list_tools()
+ get_user = next(t for t in tools if t.name == "get_user")
+ # With validate_output=False, the outputSchema should be permissive
+ assert get_user.outputSchema is not None
+ assert get_user.outputSchema.get("additionalProperties") is True
+ # Should NOT have specific properties from the original schema
+ assert "properties" not in get_user.outputSchema