Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Execute the HTTP request using RequestDirector.


### `OpenAPIResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L227" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OpenAPIResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L233" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>


Resource implementation for OpenAPI endpoints.


**Methods:**

#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L257" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L263" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
read(self) -> ResourceResult
Expand All @@ -44,15 +44,15 @@ read(self) -> ResourceResult
Fetch the resource data by making an HTTP request.


### `OpenAPIResourceTemplate` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OpenAPIResourceTemplate` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L347" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>


Resource template implementation for OpenAPI endpoints.


**Methods:**

#### `create_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L373" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `create_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L379" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource
Expand Down
4 changes: 2 additions & 2 deletions docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ spec. Each component makes HTTP calls to the described API endpoints.

**Methods:**

#### `lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L182" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
lifespan(self) -> AsyncIterator[None]
Expand All @@ -30,7 +30,7 @@ lifespan(self) -> AsyncIterator[None]
Manage the lifecycle of the auto-created httpx client.


#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L430" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L447" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
get_tasks(self) -> Sequence[FastMCPComponent]
Expand Down
14 changes: 9 additions & 5 deletions docs/python-sdk/fastmcp-server-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.


### `create_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2173" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
Expand Down Expand Up @@ -823,7 +823,7 @@ objects are imported with their original names.
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2022" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```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.
Expand All @@ -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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2067" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2073" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```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
Expand All @@ -869,7 +873,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.


#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
Expand All @@ -887,7 +891,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.


#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2165" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
generate_name(cls, name: str | None = None) -> str
Expand Down
6 changes: 6 additions & 0 deletions src/fastmcp/server/providers/openapi/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions src/fastmcp/server/providers/openapi/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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__()

Expand All @@ -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]] = {
Expand Down Expand Up @@ -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
Comment on lines +245 to +249
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve wrapping when output validation is disabled

With validate_output=False, this replaces the extracted schema with a plain object schema and only carries x-fastmcp-wrap-result forward when it already existed. For endpoints whose spec says type: object, a backend that actually returns an array/primitive will not be wrapped, so OpenAPITool.run passes a non-dict structured_content and ToolResult raises structured_content must be a dict; the tool still fails instead of acting as a validation opt-out for response shape mismatches.

Useful? React with 👍 / 👎.

output_schema = permissive

tool_name = self._get_unique_name(name, "tool")
base_description = (
route.description
Expand Down
6 changes: 6 additions & 0 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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:
Expand All @@ -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)

Expand Down
Loading