mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 10:18:08 +02:00
Convert OpenAPI provider to the OpenAPI plugin (#4015)
This commit is contained in:
parent
03f4a90e60
commit
a82979e433
26 changed files with 1700 additions and 1021 deletions
362
tests/server/plugins/openapi/test_server.py
Normal file
362
tests/server/plugins/openapi/test_server.py
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
"""Unit tests for OpenAPIProvider."""
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.server.plugins.openapi.provider import DEFAULT_TIMEOUT, OpenAPIProvider
|
||||
|
||||
|
||||
class TestOpenAPIProviderServerVariables:
|
||||
"""Test that OpenAPIProvider resolves OpenAPI 3.x server variables."""
|
||||
|
||||
def test_server_variables_substituted_with_defaults(self):
|
||||
spec = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Test API", "version": "1.0.0"},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://{region}.api.example.com/v1",
|
||||
"variables": {
|
||||
"region": {
|
||||
"default": "us",
|
||||
"enum": ["us", "eu", "apac"],
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
"paths": {},
|
||||
}
|
||||
client = OpenAPIProvider._create_default_client(spec)
|
||||
assert str(client.base_url) == "https://us.api.example.com/v1/"
|
||||
|
||||
def test_multiple_server_variables_substituted(self):
|
||||
spec = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Test API", "version": "1.0.0"},
|
||||
"servers": [
|
||||
{
|
||||
"url": "{scheme}://{host}/v1",
|
||||
"variables": {
|
||||
"scheme": {"default": "https"},
|
||||
"host": {"default": "api.example.com"},
|
||||
},
|
||||
}
|
||||
],
|
||||
"paths": {},
|
||||
}
|
||||
client = OpenAPIProvider._create_default_client(spec)
|
||||
assert str(client.base_url) == "https://api.example.com/v1/"
|
||||
|
||||
def test_static_server_url_unaffected(self):
|
||||
spec = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Test API", "version": "1.0.0"},
|
||||
"servers": [{"url": "https://api.example.com"}],
|
||||
"paths": {},
|
||||
}
|
||||
client = OpenAPIProvider._create_default_client(spec)
|
||||
assert str(client.base_url) == "https://api.example.com"
|
||||
|
||||
|
||||
class TestOpenAPIProviderBasicFunctionality:
|
||||
"""Test basic OpenAPIProvider functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def simple_openapi_spec(self):
|
||||
"""Simple OpenAPI spec for testing."""
|
||||
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 user by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": True,
|
||||
"schema": {"type": "integer"},
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer"},
|
||||
"name": {"type": "string"},
|
||||
"email": {"type": "string"},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"post": {
|
||||
"operationId": "create_user",
|
||||
"summary": "Create a new user",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"email": {"type": "string"},
|
||||
},
|
||||
"required": ["name", "email"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "User created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer"},
|
||||
"name": {"type": "string"},
|
||||
"email": {"type": "string"},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_provider_initialization(self, simple_openapi_spec):
|
||||
"""Test provider initialization with OpenAPI spec."""
|
||||
client = httpx.AsyncClient(base_url="https://api.example.com")
|
||||
provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=client)
|
||||
|
||||
# Should have initialized RequestDirector successfully
|
||||
assert hasattr(provider, "_director")
|
||||
assert hasattr(provider, "_spec")
|
||||
|
||||
def test_server_with_provider(self, simple_openapi_spec):
|
||||
"""Test server initialization with OpenAPIProvider."""
|
||||
client = httpx.AsyncClient(base_url="https://api.example.com")
|
||||
provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=client)
|
||||
|
||||
mcp = FastMCP("Test Server")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
assert mcp.name == "Test Server"
|
||||
|
||||
async def test_provider_creates_tools_from_spec(self, simple_openapi_spec):
|
||||
"""Test that provider creates tools from OpenAPI spec."""
|
||||
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
|
||||
provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=client)
|
||||
|
||||
mcp = FastMCP("Test Server")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
async with Client(mcp) as mcp_client:
|
||||
tools = await mcp_client.list_tools()
|
||||
|
||||
# Should have created tools for both operations
|
||||
assert len(tools) == 2
|
||||
|
||||
tool_names = {tool.name for tool in tools}
|
||||
assert "get_user" in tool_names
|
||||
assert "create_user" in tool_names
|
||||
|
||||
async def test_provider_tool_execution(self, simple_openapi_spec):
|
||||
"""Test tool execution uses RequestDirector."""
|
||||
mock_client = httpx.AsyncClient()
|
||||
provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=mock_client)
|
||||
|
||||
mcp = FastMCP("Test Server")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
async with Client(mcp) as mcp_client:
|
||||
tools = await mcp_client.list_tools()
|
||||
|
||||
# Should have tools using RequestDirector
|
||||
assert len(tools) == 2
|
||||
|
||||
get_user_tool = next(tool for tool in tools if tool.name == "get_user")
|
||||
assert get_user_tool is not None
|
||||
assert get_user_tool.description is not None
|
||||
|
||||
def test_provider_creates_default_client_from_spec(self, simple_openapi_spec):
|
||||
"""Test that omitting client creates one from the spec's servers URL."""
|
||||
provider = OpenAPIProvider(openapi_spec=simple_openapi_spec)
|
||||
assert str(provider._client.base_url).rstrip("/") == "https://api.example.com"
|
||||
assert provider._client.timeout == httpx.Timeout(DEFAULT_TIMEOUT)
|
||||
|
||||
def test_provider_default_client_requires_servers(self):
|
||||
"""Test that omitting client without servers in spec raises."""
|
||||
spec = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "No Servers", "version": "1.0.0"},
|
||||
"paths": {},
|
||||
}
|
||||
with pytest.raises(ValueError, match="No server URL"):
|
||||
OpenAPIProvider(openapi_spec=spec)
|
||||
|
||||
def test_provider_with_empty_spec(self):
|
||||
"""Test provider with minimal OpenAPI spec."""
|
||||
minimal_spec = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Empty API", "version": "1.0.0"},
|
||||
"paths": {},
|
||||
}
|
||||
|
||||
client = httpx.AsyncClient(base_url="https://api.example.com")
|
||||
provider = OpenAPIProvider(openapi_spec=minimal_spec, client=client)
|
||||
|
||||
# Should handle empty paths gracefully
|
||||
assert hasattr(provider, "_director")
|
||||
assert hasattr(provider, "_spec")
|
||||
|
||||
async def test_clean_schema_output_no_unused_defs(self):
|
||||
"""Test that unused schema definitions are removed from tool schemas."""
|
||||
spec_with_unused_defs = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Test API", "version": "1.0.0"},
|
||||
"servers": [{"url": "https://api.example.com"}],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"post": {
|
||||
"operationId": "create_user",
|
||||
"summary": "Create a new user",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "title": "Name"},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"title": "Active",
|
||||
},
|
||||
},
|
||||
"required": ["name", "active"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name",
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"title": "Active",
|
||||
},
|
||||
},
|
||||
"required": ["id", "name", "active"],
|
||||
"title": "User",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
}
|
||||
},
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
||||
},
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
"required": ["loc", "msg", "type"],
|
||||
"title": "ValidationError",
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
|
||||
provider = OpenAPIProvider(
|
||||
openapi_spec=spec_with_unused_defs, client=client
|
||||
)
|
||||
mcp = FastMCP("Test Server")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
async with Client(mcp) as mcp_client:
|
||||
tools = await mcp_client.list_tools()
|
||||
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
|
||||
assert tool.name == "create_user"
|
||||
|
||||
expected_input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "title": "Name"},
|
||||
"active": {"type": "boolean", "title": "Active"},
|
||||
},
|
||||
"required": ["name", "active"],
|
||||
}
|
||||
assert tool.inputSchema == expected_input_schema
|
||||
|
||||
expected_output_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer", "title": "Id"},
|
||||
"name": {"type": "string", "title": "Name"},
|
||||
"active": {"type": "boolean", "title": "Active"},
|
||||
},
|
||||
"required": ["id", "name", "active"],
|
||||
"title": "User",
|
||||
}
|
||||
assert tool.outputSchema == expected_output_schema
|
||||
Loading…
Add table
Add a link
Reference in a new issue