Skip to content

Commit 9388b69

Browse files
committed
basic tests and fixes
1 parent bc3738c commit 9388b69

File tree

5 files changed

+333
-15
lines changed

5 files changed

+333
-15
lines changed

fastapi_mcp/server.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def __init__(
3131
self.tools: List[types.Tool]
3232

3333
self.fastapi = fastapi
34-
self.name = name
35-
self.description = description
34+
self.name = name or self.fastapi.title or "FastAPI MCP"
35+
self.description = description or self.fastapi.description
3636

3737
self._base_url = base_url
3838
self._describe_all_responses = describe_all_responses
@@ -68,10 +68,6 @@ def create_server(self) -> Server:
6868
routes=self.fastapi.routes,
6969
)
7070

71-
# Get server name and description from app if not provided
72-
server_name = self.name or self.fastapi.title or "FastAPI MCP"
73-
server_description = self.description or self.fastapi.description
74-
7571
# Convert OpenAPI schema to MCP tools
7672
self.tools, self.operation_map = convert_openapi_to_mcp_tools(
7773
openapi_schema,
@@ -98,7 +94,7 @@ def create_server(self) -> Server:
9894
self._base_url = self._base_url[:-1]
9995

10096
# Create the MCP server
101-
mcp_server: Server = Server(server_name, server_description)
97+
mcp_server: Server = Server(self.name, self.description)
10298

10399
# Create a lifespan context manager to store the base_url and operation_map
104100
@asynccontextmanager

tests/conftest.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,59 @@
44
# Add the parent directory to the path
55
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
66

7-
from .fixtures import types # noqa: F401
8-
from .fixtures import example_data # noqa: F401
9-
from .fixtures import simple_app # noqa: F401
10-
from .fixtures import complex_app # noqa: F401
7+
from .fixtures.types import * # noqa: F403
8+
from .fixtures.example_data import * # noqa: F403
9+
from .fixtures.simple_app import * # noqa: F403
10+
from .fixtures.complex_app import * # noqa: F403
11+
12+
# Add specific fixtures for MCP testing
13+
import pytest
14+
from fastapi.testclient import TestClient
15+
16+
from fastapi_mcp import FastApiMCP
17+
18+
19+
@pytest.fixture
20+
def mcp_server(simple_fastapi_app):
21+
"""
22+
Create a basic MCP server instance for the simple_fastapi_app.
23+
This is a utility fixture to be used by multiple tests.
24+
"""
25+
return FastApiMCP(
26+
simple_fastapi_app,
27+
name="Test MCP Server",
28+
description="Test MCP server for unit testing",
29+
base_url="http://testserver",
30+
)
31+
32+
33+
@pytest.fixture
34+
def complex_mcp_server(complex_fastapi_app):
35+
"""
36+
Create a MCP server instance for the complex_fastapi_app.
37+
This is a utility fixture to be used by multiple tests.
38+
"""
39+
return FastApiMCP(
40+
complex_fastapi_app,
41+
name="Complex Test MCP Server",
42+
description="Complex test MCP server for unit testing",
43+
base_url="http://testserver",
44+
describe_all_responses=True,
45+
describe_full_response_schema=True,
46+
)
47+
48+
49+
@pytest.fixture
50+
def client(simple_fastapi_app):
51+
"""
52+
Create a test client for the simple_fastapi_app.
53+
"""
54+
return TestClient(simple_fastapi_app)
55+
56+
57+
@pytest.fixture
58+
def complex_client(complex_fastapi_app):
59+
"""
60+
Create a test client for the complex_fastapi_app.
61+
"""
62+
return TestClient(complex_fastapi_app)

tests/fixtures/simple_app.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ async def list_items(
2121
sort_by: Optional[str] = Query(None, description="Field to sort by"),
2222
):
2323
"""List all items with pagination and sorting options."""
24-
return []
24+
return [
25+
Item(id=1, name="Item 1", price=10.0, tags=["tag1", "tag2"], description="Item 1 description"),
26+
Item(id=2, name="Item 2", price=20.0, tags=["tag2", "tag3"]),
27+
Item(id=3, name="Item 3", price=30.0, tags=["tag3", "tag4"], description="Item 3 description"),
28+
]
2529

2630
@app.get("/items/{item_id}", response_model=Item, tags=["items"], operation_id="get_item")
2731
async def read_item(
2832
item_id: int = Path(..., description="The ID of the item to retrieve"),
2933
include_details: bool = Query(False, description="Include additional details"),
3034
):
3135
"""Get a specific item by its ID with optional details."""
32-
return {"id": item_id, "name": "Test Item", "price": 10.0}
36+
return Item(id=item_id, name="Test Item", price=10.0, tags=["tag1", "tag2"])
3337

3438
@app.post("/items/", response_model=Item, tags=["items"], operation_id="create_item")
3539
async def create_item(item: Item = Body(..., description="The item to create")):
@@ -45,9 +49,9 @@ async def update_item(
4549
item.id = item_id
4650
return item
4751

48-
@app.delete("/items/{item_id}", tags=["items"], operation_id="delete_item")
52+
@app.delete("/items/{item_id}", status_code=204, tags=["items"], operation_id="delete_item")
4953
async def delete_item(item_id: int = Path(..., description="The ID of the item to delete")):
5054
"""Delete an item from the database."""
51-
return {"message": "Item deleted successfully"}
55+
return None
5256

5357
return app

tests/test_basic_functionality.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from fastapi import FastAPI
2+
from mcp.server.lowlevel.server import Server
3+
4+
from fastapi_mcp import FastApiMCP
5+
6+
7+
def test_create_mcp_server(simple_fastapi_app: FastAPI):
8+
"""Test creating an MCP server without mounting it."""
9+
mcp = FastApiMCP(
10+
simple_fastapi_app,
11+
name="Test MCP Server",
12+
description="Test description",
13+
base_url="http://localhost:8000",
14+
)
15+
16+
# Verify the MCP server was created correctly
17+
assert mcp.name == "Test MCP Server"
18+
assert mcp.description == "Test description"
19+
assert mcp._base_url == "http://localhost:8000"
20+
assert isinstance(mcp.server, Server)
21+
assert len(mcp.tools) > 0, "Should have extracted tools from the app"
22+
assert len(mcp.operation_map) > 0, "Should have operation mapping"
23+
24+
# Check that the operation map contains all expected operations from simple_app
25+
expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item"]
26+
for op in expected_operations:
27+
assert op in mcp.operation_map, f"Operation {op} not found in operation map"
28+
29+
30+
def test_default_values(simple_fastapi_app: FastAPI):
31+
"""Test that default values are used when not explicitly provided."""
32+
mcp = FastApiMCP(simple_fastapi_app)
33+
34+
# Verify default values
35+
assert mcp.name == simple_fastapi_app.title
36+
assert mcp.description == simple_fastapi_app.description
37+
38+
# Default base URL should be derived or defaulted
39+
assert mcp._base_url is not None
40+
41+
# Mount with default path
42+
mcp.mount()
43+
44+
# Check that the MCP server was properly mounted
45+
# Look for a route that includes our mount path in its pattern
46+
route_found = any("/mcp" in str(route) for route in simple_fastapi_app.routes)
47+
assert route_found, "MCP server mount point not found in app routes"
48+
49+
50+
def test_normalize_paths(simple_fastapi_app: FastAPI):
51+
"""Test that mount paths are normalized correctly."""
52+
mcp = FastApiMCP(simple_fastapi_app)
53+
54+
# Test with path without leading slash
55+
mount_path = "test-mcp"
56+
mcp.mount(mount_path=mount_path)
57+
58+
# Check that the route was added with a normalized path
59+
route_found = any("/test-mcp" in str(route) for route in simple_fastapi_app.routes)
60+
assert route_found, "Normalized mount path not found in app routes"
61+
62+
# Create a new MCP server
63+
mcp2 = FastApiMCP(simple_fastapi_app)
64+
65+
# Test with path with trailing slash
66+
mount_path = "/test-mcp2/"
67+
mcp2.mount(mount_path=mount_path)
68+
69+
# Check that the route was added with a normalized path
70+
route_found = any("/test-mcp2" in str(route) for route in simple_fastapi_app.routes)
71+
assert route_found, "Normalized mount path not found in app routes"

tests/test_configuration.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
from fastapi import FastAPI
2+
3+
from fastapi_mcp import FastApiMCP
4+
5+
6+
def test_default_configuration(simple_fastapi_app: FastAPI):
7+
"""Test the default configuration of FastApiMCP."""
8+
# Create MCP server with defaults
9+
mcp_server = FastApiMCP(simple_fastapi_app)
10+
11+
# Check default name and description
12+
assert mcp_server.name == simple_fastapi_app.title
13+
assert mcp_server.description == simple_fastapi_app.description
14+
15+
# Check default base URL
16+
assert mcp_server._base_url is not None
17+
assert mcp_server._base_url.startswith("http://")
18+
19+
# Check default options
20+
assert mcp_server._describe_all_responses is False
21+
assert mcp_server._describe_full_response_schema is False
22+
23+
24+
def test_custom_configuration(simple_fastapi_app: FastAPI):
25+
"""Test a custom configuration of FastApiMCP."""
26+
# Create MCP server with custom options
27+
custom_name = "Custom MCP Server"
28+
custom_description = "A custom MCP server for testing"
29+
custom_base_url = "https://custom-api.example.com"
30+
31+
mcp_server = FastApiMCP(
32+
simple_fastapi_app,
33+
name=custom_name,
34+
description=custom_description,
35+
base_url=custom_base_url,
36+
describe_all_responses=True,
37+
describe_full_response_schema=True,
38+
)
39+
40+
# Check custom name and description
41+
assert mcp_server.name == custom_name
42+
assert mcp_server.description == custom_description
43+
44+
# Check custom base URL
45+
assert mcp_server._base_url == custom_base_url
46+
47+
# Check custom options
48+
assert mcp_server._describe_all_responses is True
49+
assert mcp_server._describe_full_response_schema is True
50+
51+
52+
def test_base_url_normalization(simple_fastapi_app: FastAPI):
53+
"""Test that base URLs are normalized correctly."""
54+
# Test with trailing slash
55+
mcp_server1 = FastApiMCP(
56+
simple_fastapi_app,
57+
base_url="http://example.com/api/",
58+
)
59+
assert mcp_server1._base_url == "http://example.com/api"
60+
61+
# Test without trailing slash
62+
mcp_server2 = FastApiMCP(
63+
simple_fastapi_app,
64+
base_url="http://example.com/api",
65+
)
66+
assert mcp_server2._base_url == "http://example.com/api"
67+
68+
69+
def test_describe_all_responses_config_simple_app(simple_fastapi_app: FastAPI):
70+
"""Test the describe_all_responses behavior with the simple app."""
71+
mcp_default = FastApiMCP(
72+
simple_fastapi_app,
73+
base_url="http://example.com",
74+
)
75+
76+
mcp_all_responses = FastApiMCP(
77+
simple_fastapi_app,
78+
base_url="http://example.com",
79+
describe_all_responses=True,
80+
)
81+
82+
for tool in mcp_default.tools:
83+
assert tool.description is not None
84+
if tool.name != "delete_item":
85+
assert tool.description.count("**200**") == 1, "The description should contain a 200 status code"
86+
assert tool.description.count("**Example Response:**") == 1, (
87+
"The description should only contain one example response"
88+
)
89+
assert tool.description.count("**Output Schema:**") == 0, (
90+
"The description should not contain a full output schema"
91+
)
92+
else:
93+
# The delete endpoint in the Items API returns a 204 status code
94+
assert tool.description.count("**200**") == 0, "The description should not contain a 200 status code"
95+
assert tool.description.count("**204**") == 1, "The description should contain a 204 status code"
96+
# The delete endpoint in the Items API returns a 204 status code and has no response body
97+
assert tool.description.count("**Example Response:**") == 0, (
98+
"The description should not contain any example responses"
99+
)
100+
assert tool.description.count("**Output Schema:**") == 0, (
101+
"The description should not contain a full output schema"
102+
)
103+
104+
for tool in mcp_all_responses.tools:
105+
assert tool.description is not None
106+
if tool.name != "delete_item":
107+
assert tool.description.count("**200**") == 1, "The description should contain a 200 status code"
108+
assert tool.description.count("**422**") == 1, "The description should contain a 422 status code"
109+
assert tool.description.count("**Example Response:**") == 2, (
110+
"The description should contain two example responses"
111+
)
112+
assert tool.description.count("**Output Schema:**") == 0, (
113+
"The description should not contain a full output schema"
114+
)
115+
else:
116+
assert tool.description.count("**204**") == 1, "The description should contain a 204 status code"
117+
assert tool.description.count("**422**") == 1, "The description should contain a 422 status code"
118+
# The delete endpoint in the Items API returns a 204 status code and has no response body
119+
# But FastAPI's default 422 response should be present
120+
# So just 1 instance of Example Response should be present
121+
assert tool.description.count("**Example Response:**") == 1, (
122+
"The description should contain one example response"
123+
)
124+
assert tool.description.count("**Output Schema:**") == 0, (
125+
"The description should not contain any output schema"
126+
)
127+
128+
129+
def test_describe_full_response_schema_config_simple_app(simple_fastapi_app: FastAPI):
130+
"""Test the describe_full_response_schema behavior with the simple app."""
131+
132+
mcp_full_response_schema = FastApiMCP(
133+
simple_fastapi_app,
134+
base_url="http://example.com",
135+
describe_full_response_schema=True,
136+
)
137+
138+
for tool in mcp_full_response_schema.tools:
139+
assert tool.description is not None
140+
if tool.name != "delete_item":
141+
assert tool.description.count("**200**") == 1, "The description should contain a 200 status code"
142+
assert tool.description.count("**Example Response:**") == 1, (
143+
"The description should only contain one example response"
144+
)
145+
assert tool.description.count("**Output Schema:**") == 1, (
146+
"The description should contain one full output schema"
147+
)
148+
else:
149+
# The delete endpoint in the Items API returns a 204 status code
150+
assert tool.description.count("**200**") == 0, "The description should not contain a 200 status code"
151+
assert tool.description.count("**204**") == 1, "The description should contain a 204 status code"
152+
# The delete endpoint in the Items API returns a 204 status code and has no response body
153+
assert tool.description.count("**Example Response:**") == 0, (
154+
"The description should not contain any example responses"
155+
)
156+
assert tool.description.count("**Output Schema:**") == 0, (
157+
"The description should not contain a full output schema"
158+
)
159+
160+
161+
def test_describe_all_responses_and_full_response_schema_config_simple_app(simple_fastapi_app: FastAPI):
162+
"""Test the describe_all_responses and describe_full_response_schema params together with the simple app."""
163+
164+
mcp_all_responses_and_full_response_schema = FastApiMCP(
165+
simple_fastapi_app,
166+
base_url="http://example.com",
167+
describe_all_responses=True,
168+
describe_full_response_schema=True,
169+
)
170+
171+
for tool in mcp_all_responses_and_full_response_schema.tools:
172+
assert tool.description is not None
173+
if tool.name != "delete_item":
174+
assert tool.description.count("**200**") == 1, "The description should contain a 200 status code"
175+
assert tool.description.count("**422**") == 1, "The description should contain a 422 status code"
176+
assert tool.description.count("**Example Response:**") == 2, (
177+
"The description should contain two example responses"
178+
)
179+
assert tool.description.count("**Output Schema:**") == 2, (
180+
"The description should contain two full output schemas"
181+
)
182+
else:
183+
# The delete endpoint in the Items API returns a 204 status code
184+
assert tool.description.count("**200**") == 0, "The description should not contain a 200 status code"
185+
assert tool.description.count("**204**") == 1, "The description should contain a 204 status code"
186+
assert tool.description.count("**422**") == 1, "The description should contain a 422 status code"
187+
# The delete endpoint in the Items API returns a 204 status code and has no response body
188+
# But FastAPI's default 422 response should be present
189+
# So just 1 instance of Example Response and Output Schema should be present
190+
assert tool.description.count("**Example Response:**") == 1, (
191+
"The description should contain one example response"
192+
)
193+
assert tool.description.count("**Output Schema:**") == 1, (
194+
"The description should contain one full output schema"
195+
)

0 commit comments

Comments
 (0)