Skip to content

Commit 8dd0813

Browse files
committed
write final tests, improve test coverage to 92%, and set coverage limit higher
1 parent ebbd0e4 commit 8dd0813

File tree

2 files changed

+274
-1
lines changed

2 files changed

+274
-1
lines changed

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[pytest]
2-
addopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=68.5
2+
addopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=92
33
asyncio_mode = auto
44
log_cli = true
55
log_cli_level = DEBUG

tests/test_openapi_conversion.py

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
from fastapi import FastAPI
2+
from fastapi.openapi.utils import get_openapi
3+
import mcp.types as types
4+
5+
from fastapi_mcp.openapi.convert import convert_openapi_to_mcp_tools
6+
from fastapi_mcp.openapi.utils import (
7+
clean_schema_for_display,
8+
generate_example_from_schema,
9+
get_single_param_type_from_schema,
10+
)
11+
12+
13+
def test_simple_app_conversion(simple_fastapi_app: FastAPI):
14+
openapi_schema = get_openapi(
15+
title=simple_fastapi_app.title,
16+
version=simple_fastapi_app.version,
17+
openapi_version=simple_fastapi_app.openapi_version,
18+
description=simple_fastapi_app.description,
19+
routes=simple_fastapi_app.routes,
20+
)
21+
22+
tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)
23+
24+
assert len(tools) == 6
25+
assert len(operation_map) == 6
26+
27+
expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"]
28+
for op in expected_operations:
29+
assert op in operation_map
30+
31+
for tool in tools:
32+
assert isinstance(tool, types.Tool)
33+
assert tool.name in expected_operations
34+
assert tool.description is not None
35+
assert tool.inputSchema is not None
36+
37+
38+
def test_complex_app_conversion(complex_fastapi_app: FastAPI):
39+
openapi_schema = get_openapi(
40+
title=complex_fastapi_app.title,
41+
version=complex_fastapi_app.version,
42+
openapi_version=complex_fastapi_app.openapi_version,
43+
description=complex_fastapi_app.description,
44+
routes=complex_fastapi_app.routes,
45+
)
46+
47+
tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)
48+
49+
expected_operations = ["list_products", "get_product", "create_order", "get_customer"]
50+
assert len(tools) == len(expected_operations)
51+
assert len(operation_map) == len(expected_operations)
52+
53+
for op in expected_operations:
54+
assert op in operation_map
55+
56+
for tool in tools:
57+
assert isinstance(tool, types.Tool)
58+
assert tool.name in expected_operations
59+
assert tool.description is not None
60+
assert tool.inputSchema is not None
61+
62+
63+
def test_describe_full_response_schema(simple_fastapi_app: FastAPI):
64+
openapi_schema = get_openapi(
65+
title=simple_fastapi_app.title,
66+
version=simple_fastapi_app.version,
67+
openapi_version=simple_fastapi_app.openapi_version,
68+
description=simple_fastapi_app.description,
69+
routes=simple_fastapi_app.routes,
70+
)
71+
72+
tools_full, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_full_response_schema=True)
73+
74+
tools_simple, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_full_response_schema=False)
75+
76+
for i, tool in enumerate(tools_full):
77+
assert tool.description is not None
78+
assert tools_simple[i].description is not None
79+
80+
tool_desc = tool.description or ""
81+
simple_desc = tools_simple[i].description or ""
82+
83+
assert len(tool_desc) >= len(simple_desc)
84+
85+
if tool.name == "delete_item":
86+
continue
87+
88+
assert "**Output Schema:**" in tool_desc
89+
90+
if "**Output Schema:**" in simple_desc:
91+
assert len(tool_desc) > len(simple_desc)
92+
93+
94+
def test_describe_all_responses(complex_fastapi_app: FastAPI):
95+
openapi_schema = get_openapi(
96+
title=complex_fastapi_app.title,
97+
version=complex_fastapi_app.version,
98+
openapi_version=complex_fastapi_app.openapi_version,
99+
description=complex_fastapi_app.description,
100+
routes=complex_fastapi_app.routes,
101+
)
102+
103+
tools_all, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_all_responses=True)
104+
105+
tools_success, _ = convert_openapi_to_mcp_tools(openapi_schema, describe_all_responses=False)
106+
107+
create_order_all = next(t for t in tools_all if t.name == "create_order")
108+
create_order_success = next(t for t in tools_success if t.name == "create_order")
109+
110+
assert create_order_all.description is not None
111+
assert create_order_success.description is not None
112+
113+
all_desc = create_order_all.description or ""
114+
success_desc = create_order_success.description or ""
115+
116+
assert "400" in all_desc
117+
assert "404" in all_desc
118+
assert "422" in all_desc
119+
120+
assert all_desc.count("400") >= success_desc.count("400")
121+
122+
123+
def test_schema_utils():
124+
schema = {
125+
"type": "object",
126+
"properties": {
127+
"id": {"type": "integer"},
128+
"name": {"type": "string"},
129+
"tags": {"type": "array", "items": {"type": "string"}},
130+
},
131+
"required": ["id", "name"],
132+
"additionalProperties": False,
133+
"x-internal": "Some internal data",
134+
}
135+
136+
cleaned = clean_schema_for_display(schema)
137+
138+
assert "required" in cleaned
139+
assert "properties" in cleaned
140+
assert "type" in cleaned
141+
142+
example = generate_example_from_schema(schema)
143+
assert "id" in example
144+
assert "name" in example
145+
assert "tags" in example
146+
assert isinstance(example["id"], int)
147+
assert isinstance(example["name"], str)
148+
assert isinstance(example["tags"], list)
149+
150+
assert get_single_param_type_from_schema({"type": "string"}) == "string"
151+
assert get_single_param_type_from_schema({"type": "array", "items": {"type": "string"}}) == "array"
152+
153+
array_schema = {"type": "array", "items": {"type": "string", "enum": ["red", "green", "blue"]}}
154+
array_example = generate_example_from_schema(array_schema)
155+
assert isinstance(array_example, list)
156+
assert len(array_example) > 0
157+
158+
assert isinstance(array_example[0], str)
159+
160+
161+
def test_parameter_handling(complex_fastapi_app: FastAPI):
162+
openapi_schema = get_openapi(
163+
title=complex_fastapi_app.title,
164+
version=complex_fastapi_app.version,
165+
openapi_version=complex_fastapi_app.openapi_version,
166+
description=complex_fastapi_app.description,
167+
routes=complex_fastapi_app.routes,
168+
)
169+
170+
tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)
171+
172+
list_products_tool = next(tool for tool in tools if tool.name == "list_products")
173+
174+
properties = list_products_tool.inputSchema["properties"]
175+
176+
assert "product_id" not in properties # This is from get_product, not list_products
177+
178+
assert "category" in properties
179+
assert properties["category"].get("type") == "string" # Enum converted to string
180+
assert "description" in properties["category"]
181+
assert "Filter by product category" in properties["category"]["description"]
182+
183+
assert "min_price" in properties
184+
assert properties["min_price"].get("type") == "number"
185+
assert "description" in properties["min_price"]
186+
assert "Minimum price filter" in properties["min_price"]["description"]
187+
if "minimum" in properties["min_price"]:
188+
assert properties["min_price"]["minimum"] > 0 # gt=0 in Query param
189+
190+
assert "in_stock_only" in properties
191+
assert properties["in_stock_only"].get("type") == "boolean"
192+
assert properties["in_stock_only"].get("default") is False # Default value preserved
193+
194+
assert "page" in properties
195+
assert properties["page"].get("type") == "integer"
196+
assert properties["page"].get("default") == 1 # Default value preserved
197+
if "minimum" in properties["page"]:
198+
assert properties["page"]["minimum"] >= 1 # ge=1 in Query param
199+
200+
assert "size" in properties
201+
assert properties["size"].get("type") == "integer"
202+
if "minimum" in properties["size"] and "maximum" in properties["size"]:
203+
assert properties["size"]["minimum"] >= 1 # ge=1 in Query param
204+
assert properties["size"]["maximum"] <= 100 # le=100 in Query param
205+
206+
assert "tag" in properties
207+
assert properties["tag"].get("type") == "array"
208+
209+
required = list_products_tool.inputSchema.get("required", [])
210+
assert "page" not in required # Has default value
211+
assert "category" not in required # Optional parameter
212+
213+
assert "list_products" in operation_map
214+
assert operation_map["list_products"]["path"] == "/products"
215+
assert operation_map["list_products"]["method"] == "get"
216+
217+
get_product_tool = next(tool for tool in tools if tool.name == "get_product")
218+
get_product_props = get_product_tool.inputSchema["properties"]
219+
220+
assert "product_id" in get_product_props
221+
assert get_product_props["product_id"].get("type") == "string" # UUID converted to string
222+
assert "description" in get_product_props["product_id"]
223+
224+
get_customer_tool = next(tool for tool in tools if tool.name == "get_customer")
225+
get_customer_props = get_customer_tool.inputSchema["properties"]
226+
227+
assert "fields" in get_customer_props
228+
assert get_customer_props["fields"].get("type") == "array"
229+
if "items" in get_customer_props["fields"]:
230+
assert get_customer_props["fields"]["items"].get("type") == "string"
231+
232+
233+
def test_request_body_handling(complex_fastapi_app: FastAPI):
234+
openapi_schema = get_openapi(
235+
title=complex_fastapi_app.title,
236+
version=complex_fastapi_app.version,
237+
openapi_version=complex_fastapi_app.openapi_version,
238+
description=complex_fastapi_app.description,
239+
routes=complex_fastapi_app.routes,
240+
)
241+
242+
tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema)
243+
244+
create_order_tool = next(tool for tool in tools if tool.name == "create_order")
245+
246+
properties = create_order_tool.inputSchema["properties"]
247+
248+
assert "customer_id" in properties
249+
assert "items" in properties
250+
assert "shipping_address_id" in properties
251+
assert "payment_method" in properties
252+
assert "notes" in properties
253+
254+
required = create_order_tool.inputSchema.get("required", [])
255+
assert "customer_id" in required
256+
assert "items" in required
257+
assert "shipping_address_id" in required
258+
assert "payment_method" in required
259+
assert "notes" not in required # Optional in OrderRequest
260+
261+
assert properties["items"].get("type") == "array"
262+
if "items" in properties["items"]:
263+
item_props = properties["items"]["items"]
264+
assert item_props.get("type") == "object"
265+
if "properties" in item_props:
266+
assert "product_id" in item_props["properties"]
267+
assert "quantity" in item_props["properties"]
268+
assert "unit_price" in item_props["properties"]
269+
assert "total" in item_props["properties"]
270+
271+
assert "create_order" in operation_map
272+
assert operation_map["create_order"]["path"] == "/orders"
273+
assert operation_map["create_order"]["method"] == "post"

0 commit comments

Comments
 (0)