mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-10-23 13:21:26 +02:00
155 lines
4.8 KiB
Python
155 lines
4.8 KiB
Python
"""Request body schemas that combine a $ref with sibling properties."""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from fastmcp import Client, FastMCP
|
|
|
|
|
|
def build_spec(
|
|
body_schema: dict[str, Any], components: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"openapi": "3.1.0",
|
|
"info": {"title": "API", "version": "1"},
|
|
"paths": {
|
|
"/things": {
|
|
"post": {
|
|
"operationId": "make_thing",
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {"application/json": {"schema": body_schema}},
|
|
},
|
|
"responses": {"200": {"description": "OK"}},
|
|
}
|
|
}
|
|
},
|
|
"components": {"schemas": components},
|
|
}
|
|
|
|
|
|
async def call_make_thing(
|
|
spec: dict[str, Any], arguments: dict[str, Any]
|
|
) -> tuple[dict[str, Any], Any]:
|
|
seen: list[Any] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
seen.append(json.loads(request.content or b"null"))
|
|
return httpx.Response(200, json={"ok": True})
|
|
|
|
async with httpx.AsyncClient(
|
|
base_url="https://api.example.com",
|
|
transport=httpx.MockTransport(handler),
|
|
) as http:
|
|
server = FastMCP.from_openapi(openapi_spec=spec, client=http)
|
|
async with Client(server) as client:
|
|
tool = (await client.list_tools())[0]
|
|
await client.call_tool("make_thing", arguments)
|
|
return tool.inputSchema, seen[0]
|
|
|
|
|
|
BASE = {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"required": ["name"],
|
|
}
|
|
|
|
|
|
async def test_ref_with_sibling_properties_advertises_both_fields():
|
|
spec = build_spec(
|
|
{
|
|
"$ref": "#/components/schemas/Base",
|
|
"properties": {"local": {"type": "string"}},
|
|
},
|
|
{"Base": BASE},
|
|
)
|
|
schema, _ = await call_make_thing(spec, {"name": "n", "local": "l"})
|
|
assert set(schema["properties"]) == {"name", "local"}
|
|
|
|
|
|
async def test_ref_with_sibling_properties_sends_both_fields():
|
|
spec = build_spec(
|
|
{
|
|
"$ref": "#/components/schemas/Base",
|
|
"properties": {"local": {"type": "string"}},
|
|
},
|
|
{"Base": BASE},
|
|
)
|
|
_, body = await call_make_thing(spec, {"name": "n", "local": "l"})
|
|
assert body == {"name": "n", "local": "l"}
|
|
|
|
|
|
async def test_ref_with_sibling_properties_merges_required():
|
|
spec = build_spec(
|
|
{
|
|
"$ref": "#/components/schemas/Base",
|
|
"properties": {"local": {"type": "string"}},
|
|
"required": ["local"],
|
|
},
|
|
{"Base": BASE},
|
|
)
|
|
schema, _ = await call_make_thing(spec, {"name": "n", "local": "l"})
|
|
assert sorted(schema["required"]) == ["local", "name"]
|
|
|
|
|
|
async def test_sibling_property_wins_over_referenced_property():
|
|
spec = build_spec(
|
|
{
|
|
"$ref": "#/components/schemas/Base",
|
|
"properties": {"name": {"type": "integer"}},
|
|
},
|
|
{"Base": BASE},
|
|
)
|
|
schema, body = await call_make_thing(spec, {"name": 3})
|
|
assert schema["properties"]["name"]["type"] == "integer"
|
|
assert body == {"name": 3}
|
|
|
|
|
|
async def test_ref_chain_with_sibling_properties_sends_all_fields():
|
|
spec = build_spec(
|
|
{
|
|
"$ref": "#/components/schemas/Middle",
|
|
"properties": {"local": {"type": "string"}},
|
|
},
|
|
{
|
|
"Base": BASE,
|
|
"Middle": {
|
|
"$ref": "#/components/schemas/Base",
|
|
"properties": {"middle": {"type": "string"}},
|
|
},
|
|
},
|
|
)
|
|
schema, body = await call_make_thing(
|
|
spec, {"name": "n", "middle": "m", "local": "l"}
|
|
)
|
|
assert set(schema["properties"]) == {"name", "middle", "local"}
|
|
assert body == {"name": "n", "middle": "m", "local": "l"}
|
|
|
|
|
|
async def test_nested_reference_in_referenced_property_is_resolved():
|
|
spec = build_spec(
|
|
{
|
|
"$ref": "#/components/schemas/Base",
|
|
"properties": {"local": {"type": "string"}},
|
|
},
|
|
{
|
|
"Base": {
|
|
"type": "object",
|
|
"properties": {"child": {"$ref": "#/components/schemas/Child"}},
|
|
},
|
|
"Child": {"type": "object", "properties": {"id": {"type": "integer"}}},
|
|
},
|
|
)
|
|
schema, body = await call_make_thing(spec, {"child": {"id": 1}, "local": "l"})
|
|
assert schema["properties"]["child"]["properties"] == {"id": {"type": "integer"}}
|
|
assert body == {"child": {"id": 1}, "local": "l"}
|
|
|
|
|
|
async def test_ref_without_siblings_exposes_referenced_fields():
|
|
spec = build_spec({"$ref": "#/components/schemas/Base"}, {"Base": BASE})
|
|
schema, body = await call_make_thing(spec, {"name": "n"})
|
|
assert set(schema["properties"]) == {"name"}
|
|
assert schema["required"] == ["name"]
|
|
assert body == {"name": "n"}
|