fastmcp/tests/utilities/openapi/test_openapi_output_schemas.py
Jeremiah Lowin 6c9263001b
Add output schema support for OpenAPI tools (#1073)
* Add output schema support for OpenAPI tools

Implement automatic output schema extraction from OpenAPI responses for
FastMCP tools, addressing issue #1070. Tools generated from OpenAPI specs
now have meaningful output schemas instead of null, improving agent
efficiency when working with structured API responses.

- Add extract_output_schema_from_responses() function to parse response schemas
- Enhance OpenAPITool to accept and use output schemas with proper wrapping
- Automatically wrap non-object responses to comply with MCP requirements
- Include schema definitions and compress unused ones
- Add comprehensive test suite covering object, array, and primitive responses
- Update existing tests to handle structured output with proper typing
- Maintain backward compatibility for specs without response schemas

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Move output schema tests to dedicated test file

Reorganize output schema tests into test_openapi_output_schemas.py to keep
the main test_openapi.py file focused and smaller, as requested. Added two
additional tests for schema definitions handling.

- Move all extract_output_schema_from_responses() tests to new file
- Add tests for schema definitions inclusion
- Remove output schema imports from main test file

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-07 12:51:01 -04:00

236 lines
8 KiB
Python

"""Tests for OpenAPI output schema extraction functionality."""
from fastmcp.utilities.openapi import (
ResponseInfo,
extract_output_schema_from_responses,
)
class TestExtractOutputSchema:
"""Test the extract_output_schema_from_responses function."""
def test_extract_object_schema(self):
"""Test extracting object output schema (no wrapping needed)."""
responses = {
"200": ResponseInfo(
description="Success",
content_schema={
"application/json": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
},
"required": ["id", "name"],
}
},
)
}
result = extract_output_schema_from_responses(responses)
assert result == {
"type": "object",
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
"required": ["id", "name"],
}
assert result is not None and "x-fastmcp-wrap-result" not in result
def test_extract_array_schema_with_wrapping(self):
"""Test extracting array output schema (should be wrapped)."""
responses = {
"200": ResponseInfo(
description="Success",
content_schema={
"application/json": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
},
},
}
},
)
}
result = extract_output_schema_from_responses(responses)
assert result == {
"type": "object",
"properties": {
"result": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
},
},
}
},
"required": ["result"],
"x-fastmcp-wrap-result": True,
}
def test_extract_primitive_schema_with_wrapping(self):
"""Test extracting primitive output schema (should be wrapped)."""
responses = {
"201": ResponseInfo(
description="Created",
content_schema={
"application/json": {
"type": "string",
"description": "ID of created resource",
}
},
)
}
result = extract_output_schema_from_responses(responses)
assert result == {
"type": "object",
"properties": {
"result": {"type": "string", "description": "ID of created resource"}
},
"required": ["result"],
"x-fastmcp-wrap-result": True,
}
def test_priority_of_success_codes(self):
"""Test that 200 takes priority over other success codes."""
responses = {
"201": ResponseInfo(
description="Created",
content_schema={"application/json": {"type": "string"}},
),
"200": ResponseInfo(
description="Success",
content_schema={
"application/json": {
"type": "object",
"properties": {"id": {"type": "integer"}},
}
},
),
}
result = extract_output_schema_from_responses(responses)
# Should use the 200 response (object), not 201 (string)
assert result is not None and result["type"] == "object"
assert result is not None and "x-fastmcp-wrap-result" not in result
def test_prefer_json_content_type(self):
"""Test that application/json is preferred over other content types."""
responses = {
"200": ResponseInfo(
description="Success",
content_schema={
"text/plain": {"type": "string"},
"application/json": {
"type": "object",
"properties": {"id": {"type": "integer"}},
},
},
)
}
result = extract_output_schema_from_responses(responses)
# Should use the application/json schema (object), not text/plain (string)
assert result is not None and result["type"] == "object"
assert result is not None and "x-fastmcp-wrap-result" not in result
def test_no_responses(self):
"""Test that None is returned when no responses are provided."""
result = extract_output_schema_from_responses({})
assert result is None
def test_no_success_responses(self):
"""Test that None is returned when no success responses are found."""
responses = {
"400": ResponseInfo(
description="Bad Request",
content_schema={
"application/json": {
"type": "object",
"properties": {"error": {"type": "string"}},
}
},
)
}
result = extract_output_schema_from_responses(responses)
assert result is None
def test_no_content_schema(self):
"""Test that None is returned when response has no content schema."""
responses = {"204": ResponseInfo(description="No Content")}
result = extract_output_schema_from_responses(responses)
assert result is None
def test_schema_definitions_included(self):
"""Test that schema definitions are properly included in output schema."""
responses = {
"200": ResponseInfo(
description="Success",
content_schema={
"application/json": {
"type": "object",
"properties": {"user": {"$ref": "#/$defs/User"}},
}
},
)
}
schema_definitions = {
"User": {
"type": "object",
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
"required": ["id", "name"],
}
}
result = extract_output_schema_from_responses(responses, schema_definitions)
assert result is not None
assert "$defs" in result
assert "User" in result["$defs"]
assert result["$defs"]["User"] == schema_definitions["User"]
def test_wrapped_schema_with_definitions(self):
"""Test that wrapped schemas properly include schema definitions."""
responses = {
"200": ResponseInfo(
description="Success",
content_schema={
"application/json": {
"type": "array",
"items": {"$ref": "#/$defs/User"},
}
},
)
}
schema_definitions = {
"User": {
"type": "object",
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
"required": ["id", "name"],
}
}
result = extract_output_schema_from_responses(responses, schema_definitions)
assert result is not None
assert result["x-fastmcp-wrap-result"] is True
assert "$defs" in result
assert "User" in result["$defs"]
assert result["properties"]["result"]["type"] == "array"
assert result["properties"]["result"]["items"]["$ref"] == "#/$defs/User"