Spaces:
Running
Running
Merge pull request #788 from jlowin/openapi
Browse files- docs/servers/openapi.mdx +18 -37
- src/fastmcp/server/openapi.py +3 -9
- src/fastmcp/server/server.py +2 -34
- tests/client/test_openapi.py +11 -0
- tests/server/openapi/test_openapi.py +197 -113
- tests/server/openapi/test_route_map_fn.py +3 -3
docs/servers/openapi.mdx
CHANGED
|
@@ -41,17 +41,9 @@ That's it! Your entire API is now available as an MCP server. Clients can discov
|
|
| 41 |
|
| 42 |
## Route Mapping
|
| 43 |
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
-
FastMCP analyzes your API specification and automatically creates MCP components based on HTTP semantics and REST conventions. By default, the following rules are used to determine what MCP component to create for each route:
|
| 47 |
-
|
| 48 |
-
| OpenAPI Route | Example | MCP Component |
|
| 49 |
-
|---------------|---------|---------------|
|
| 50 |
-
| `GET` with path params | `GET /users/{id}` | **Resource Template** |
|
| 51 |
-
| `GET` without path params | `GET /stats` | **Resource** |
|
| 52 |
-
| `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | **Tool** |
|
| 53 |
-
|
| 54 |
-
Interally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types.
|
| 55 |
|
| 56 |
Each `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely.
|
| 57 |
|
|
@@ -60,33 +52,14 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well
|
|
| 60 |
- **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
|
| 61 |
- **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)
|
| 62 |
|
| 63 |
-
|
| 64 |
|
| 65 |
```python
|
| 66 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 67 |
|
| 68 |
DEFAULT_ROUTE_MAPPINGS = [
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
RouteMap(
|
| 72 |
-
methods=["GET"],
|
| 73 |
-
pattern=r".*\{.*\}.*",
|
| 74 |
-
mcp_type=MCPType.RESOURCE_TEMPLATE
|
| 75 |
-
),
|
| 76 |
-
|
| 77 |
-
# GET without path parameters → Resource
|
| 78 |
-
RouteMap(
|
| 79 |
-
methods=["GET"],
|
| 80 |
-
pattern=r".*",
|
| 81 |
-
mcp_type=MCPType.RESOURCE
|
| 82 |
-
),
|
| 83 |
-
|
| 84 |
-
# All other methods → Tool
|
| 85 |
-
RouteMap(
|
| 86 |
-
methods=["*"],
|
| 87 |
-
pattern=r".*",
|
| 88 |
-
mcp_type=MCPType.TOOL
|
| 89 |
-
),
|
| 90 |
]
|
| 91 |
```
|
| 92 |
|
|
@@ -94,20 +67,28 @@ DEFAULT_ROUTE_MAPPINGS = [
|
|
| 94 |
|
| 95 |
When creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map.
|
| 96 |
|
| 97 |
-
For example,
|
| 98 |
|
| 99 |
-
```python {
|
| 100 |
from fastmcp import FastMCP
|
| 101 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
mcp = FastMCP.from_openapi(
|
| 104 |
...,
|
| 105 |
-
route_maps=
|
| 106 |
-
RouteMap(mcp_type=MCPType.TOOL),
|
| 107 |
-
],
|
| 108 |
)
|
| 109 |
```
|
| 110 |
|
|
|
|
|
|
|
| 111 |
Here is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged "internal". All other routes will be handled by the default rules:
|
| 112 |
|
| 113 |
```python
|
|
|
|
| 41 |
|
| 42 |
## Route Mapping
|
| 43 |
|
| 44 |
+
By default, FastMCP converts **every endpoint** in your OpenAPI specification into an MCP **Tool**. This provides a simple, predictable starting point that ensures all your API's functionality is immediately available to the vast majority of LLM clients which only support MCP tools.
|
| 45 |
|
| 46 |
+
While this is a pragmatic default for maximum compatibility, you can easily customize this behavior. Internally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
Each `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely.
|
| 49 |
|
|
|
|
| 52 |
- **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
|
| 53 |
- **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)
|
| 54 |
|
| 55 |
+
Here is FastMCP's default rule:
|
| 56 |
|
| 57 |
```python
|
| 58 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 59 |
|
| 60 |
DEFAULT_ROUTE_MAPPINGS = [
|
| 61 |
+
# All routes become tools
|
| 62 |
+
RouteMap(mcp_type=MCPType.TOOL),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
]
|
| 64 |
```
|
| 65 |
|
|
|
|
| 67 |
|
| 68 |
When creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map.
|
| 69 |
|
| 70 |
+
For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `Resource` and `ResourceTemplate` components based on whether they had path parameters. (This was changed solely for client compatibility reasons.) You can restore this behavior by providing custom route maps:
|
| 71 |
|
| 72 |
+
```python {2, 5-10}
|
| 73 |
from fastmcp import FastMCP
|
| 74 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 75 |
|
| 76 |
+
# Restore pre-2.8.0 semantic mapping
|
| 77 |
+
semantic_maps = [
|
| 78 |
+
# GET requests with path parameters become ResourceTemplates
|
| 79 |
+
RouteMap(methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE),
|
| 80 |
+
# All other GET requests become Resources
|
| 81 |
+
RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
mcp = FastMCP.from_openapi(
|
| 85 |
...,
|
| 86 |
+
route_maps=semantic_maps,
|
|
|
|
|
|
|
| 87 |
)
|
| 88 |
```
|
| 89 |
|
| 90 |
+
With these maps, `GET` requests are handled semantically, and all other methods (`POST`, `PUT`, etc.) will fall through to the default rule and become `Tool`s.
|
| 91 |
+
|
| 92 |
Here is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged "internal". All other routes will be handled by the default rules:
|
| 93 |
|
| 94 |
```python
|
src/fastmcp/server/openapi.py
CHANGED
|
@@ -155,16 +155,10 @@ class RouteMap:
|
|
| 155 |
self.route_type = self.mcp_type
|
| 156 |
|
| 157 |
|
| 158 |
-
# Default route
|
|
|
|
| 159 |
DEFAULT_ROUTE_MAPPINGS = [
|
| 160 |
-
|
| 161 |
-
RouteMap(
|
| 162 |
-
methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
|
| 163 |
-
),
|
| 164 |
-
# GET requests without path parameters go to Resource
|
| 165 |
-
RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
|
| 166 |
-
# All other HTTP methods go to Tool
|
| 167 |
-
RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
|
| 168 |
]
|
| 169 |
|
| 170 |
|
|
|
|
| 155 |
self.route_type = self.mcp_type
|
| 156 |
|
| 157 |
|
| 158 |
+
# Default route mapping: all routes become tools.
|
| 159 |
+
# Users can provide custom route_maps to override this behavior.
|
| 160 |
DEFAULT_ROUTE_MAPPINGS = [
|
| 161 |
+
RouteMap(mcp_type=MCPType.TOOL),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
]
|
| 163 |
|
| 164 |
|
src/fastmcp/server/server.py
CHANGED
|
@@ -1551,28 +1551,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1551 |
route_map_fn: OpenAPIRouteMapFn | None = None,
|
| 1552 |
mcp_component_fn: OpenAPIComponentFn | None = None,
|
| 1553 |
mcp_names: dict[str, str] | None = None,
|
| 1554 |
-
all_routes_as_tools: bool = False,
|
| 1555 |
**settings: Any,
|
| 1556 |
) -> FastMCPOpenAPI:
|
| 1557 |
"""
|
| 1558 |
Create a FastMCP server from an OpenAPI specification.
|
| 1559 |
"""
|
| 1560 |
-
from .openapi import FastMCPOpenAPI
|
| 1561 |
-
|
| 1562 |
-
# Deprecated since 2.5.0
|
| 1563 |
-
if all_routes_as_tools:
|
| 1564 |
-
warnings.warn(
|
| 1565 |
-
"The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
|
| 1566 |
-
'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
|
| 1567 |
-
DeprecationWarning,
|
| 1568 |
-
stacklevel=2,
|
| 1569 |
-
)
|
| 1570 |
-
|
| 1571 |
-
if all_routes_as_tools and route_maps:
|
| 1572 |
-
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
| 1573 |
-
|
| 1574 |
-
elif all_routes_as_tools:
|
| 1575 |
-
route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
|
| 1576 |
|
| 1577 |
return FastMCPOpenAPI(
|
| 1578 |
openapi_spec=openapi_spec,
|
|
@@ -1593,7 +1577,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1593 |
route_map_fn: OpenAPIRouteMapFn | None = None,
|
| 1594 |
mcp_component_fn: OpenAPIComponentFn | None = None,
|
| 1595 |
mcp_names: dict[str, str] | None = None,
|
| 1596 |
-
all_routes_as_tools: bool = False,
|
| 1597 |
httpx_client_kwargs: dict[str, Any] | None = None,
|
| 1598 |
**settings: Any,
|
| 1599 |
) -> FastMCPOpenAPI:
|
|
@@ -1601,22 +1584,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1601 |
Create a FastMCP server from a FastAPI application.
|
| 1602 |
"""
|
| 1603 |
|
| 1604 |
-
from .openapi import FastMCPOpenAPI
|
| 1605 |
-
|
| 1606 |
-
# Deprecated since 2.5.0
|
| 1607 |
-
if all_routes_as_tools:
|
| 1608 |
-
warnings.warn(
|
| 1609 |
-
"The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
|
| 1610 |
-
'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
|
| 1611 |
-
DeprecationWarning,
|
| 1612 |
-
stacklevel=2,
|
| 1613 |
-
)
|
| 1614 |
-
|
| 1615 |
-
if all_routes_as_tools and route_maps:
|
| 1616 |
-
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
| 1617 |
-
|
| 1618 |
-
elif all_routes_as_tools:
|
| 1619 |
-
route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
|
| 1620 |
|
| 1621 |
if httpx_client_kwargs is None:
|
| 1622 |
httpx_client_kwargs = {}
|
|
|
|
| 1551 |
route_map_fn: OpenAPIRouteMapFn | None = None,
|
| 1552 |
mcp_component_fn: OpenAPIComponentFn | None = None,
|
| 1553 |
mcp_names: dict[str, str] | None = None,
|
|
|
|
| 1554 |
**settings: Any,
|
| 1555 |
) -> FastMCPOpenAPI:
|
| 1556 |
"""
|
| 1557 |
Create a FastMCP server from an OpenAPI specification.
|
| 1558 |
"""
|
| 1559 |
+
from .openapi import FastMCPOpenAPI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1560 |
|
| 1561 |
return FastMCPOpenAPI(
|
| 1562 |
openapi_spec=openapi_spec,
|
|
|
|
| 1577 |
route_map_fn: OpenAPIRouteMapFn | None = None,
|
| 1578 |
mcp_component_fn: OpenAPIComponentFn | None = None,
|
| 1579 |
mcp_names: dict[str, str] | None = None,
|
|
|
|
| 1580 |
httpx_client_kwargs: dict[str, Any] | None = None,
|
| 1581 |
**settings: Any,
|
| 1582 |
) -> FastMCPOpenAPI:
|
|
|
|
| 1584 |
Create a FastMCP server from a FastAPI application.
|
| 1585 |
"""
|
| 1586 |
|
| 1587 |
+
from .openapi import FastMCPOpenAPI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1588 |
|
| 1589 |
if httpx_client_kwargs is None:
|
| 1590 |
httpx_client_kwargs = {}
|
tests/client/test_openapi.py
CHANGED
|
@@ -6,6 +6,7 @@ from fastapi import FastAPI, Request
|
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
| 8 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
|
|
| 9 |
from fastmcp.utilities.tests import run_server_in_process
|
| 10 |
|
| 11 |
|
|
@@ -27,6 +28,16 @@ def fastmcp_server_for_headers() -> FastMCP:
|
|
| 27 |
mcp = FastMCP.from_fastapi(
|
| 28 |
app,
|
| 29 |
httpx_client_kwargs={"headers": {"x-server-header": "test-abc"}},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
)
|
| 31 |
|
| 32 |
return mcp
|
|
|
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
| 8 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
| 9 |
+
from fastmcp.server.openapi import MCPType, RouteMap
|
| 10 |
from fastmcp.utilities.tests import run_server_in_process
|
| 11 |
|
| 12 |
|
|
|
|
| 28 |
mcp = FastMCP.from_fastapi(
|
| 29 |
app,
|
| 30 |
httpx_client_kwargs={"headers": {"x-server-header": "test-abc"}},
|
| 31 |
+
route_maps=[
|
| 32 |
+
# GET requests with path parameters go to ResourceTemplate
|
| 33 |
+
RouteMap(
|
| 34 |
+
methods=["GET"],
|
| 35 |
+
pattern=r".*\{.*\}.*",
|
| 36 |
+
mcp_type=MCPType.RESOURCE_TEMPLATE,
|
| 37 |
+
),
|
| 38 |
+
# GET requests without path parameters go to Resource
|
| 39 |
+
RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
|
| 40 |
+
],
|
| 41 |
)
|
| 42 |
|
| 43 |
return mcp
|
tests/server/openapi/test_openapi.py
CHANGED
|
@@ -46,6 +46,20 @@ def users_db() -> dict[int, User]:
|
|
| 46 |
}
|
| 47 |
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
@pytest.fixture
|
| 50 |
def fastapi_app(users_db: dict[int, User]) -> FastAPI:
|
| 51 |
app = FastAPI(title="FastAPI App")
|
|
@@ -122,7 +136,7 @@ def api_client(fastapi_app: FastAPI) -> AsyncClient:
|
|
| 122 |
|
| 123 |
|
| 124 |
@pytest.fixture
|
| 125 |
-
async def
|
| 126 |
fastapi_app: FastAPI, api_client: httpx.AsyncClient
|
| 127 |
) -> FastMCPOpenAPI:
|
| 128 |
openapi_spec = fastapi_app.openapi()
|
|
@@ -131,6 +145,7 @@ async def fastmcp_openapi_server(
|
|
| 131 |
openapi_spec=openapi_spec,
|
| 132 |
client=api_client,
|
| 133 |
name="Test App",
|
|
|
|
| 134 |
)
|
| 135 |
|
| 136 |
|
|
@@ -169,6 +184,7 @@ async def test_create_openapi_server_with_timeout(
|
|
| 169 |
client=api_client,
|
| 170 |
name="Test App",
|
| 171 |
timeout=1.0,
|
|
|
|
| 172 |
)
|
| 173 |
assert server._timeout == 1.0
|
| 174 |
|
|
@@ -186,11 +202,24 @@ async def test_create_openapi_server_with_timeout(
|
|
| 186 |
|
| 187 |
|
| 188 |
class TestTools:
|
| 189 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
"""
|
| 191 |
By default, tools exclude GET methods
|
| 192 |
"""
|
| 193 |
-
async with Client(
|
| 194 |
tools = await client.list_tools()
|
| 195 |
assert len(tools) == 2
|
| 196 |
|
|
@@ -224,12 +253,14 @@ class TestTools:
|
|
| 224 |
)
|
| 225 |
|
| 226 |
async def test_call_create_user_tool(
|
| 227 |
-
self,
|
|
|
|
|
|
|
| 228 |
):
|
| 229 |
"""
|
| 230 |
The tool created by the OpenAPI server should be the same as the original
|
| 231 |
"""
|
| 232 |
-
async with Client(
|
| 233 |
tool_response = await client.call_tool(
|
| 234 |
"create_user_users_post", {"name": "David", "active": False}
|
| 235 |
)
|
|
@@ -243,19 +274,21 @@ class TestTools:
|
|
| 243 |
assert len(response.json()) == 4
|
| 244 |
|
| 245 |
# Check that the user was created via MCP
|
| 246 |
-
async with Client(
|
| 247 |
user_response = await client.read_resource("resource://get_user_users/4")
|
| 248 |
response_text = user_response[0].text # type: ignore[attr-defined]
|
| 249 |
user = json.loads(response_text)
|
| 250 |
assert user == expected_user
|
| 251 |
|
| 252 |
async def test_call_update_user_name_tool(
|
| 253 |
-
self,
|
|
|
|
|
|
|
| 254 |
):
|
| 255 |
"""
|
| 256 |
The tool created by the OpenAPI server should be the same as the original
|
| 257 |
"""
|
| 258 |
-
async with Client(
|
| 259 |
tool_response = await client.call_tool(
|
| 260 |
"update_user_name_users",
|
| 261 |
{"user_id": 1, "name": "XYZ"},
|
|
@@ -270,7 +303,7 @@ class TestTools:
|
|
| 270 |
assert expected_data in response.json()
|
| 271 |
|
| 272 |
# Check that the user was updated via MCP
|
| 273 |
-
async with Client(
|
| 274 |
user_response = await client.read_resource("resource://get_user_users/1")
|
| 275 |
response_text = user_response[0].text # type: ignore[attr-defined]
|
| 276 |
user = json.loads(response_text)
|
|
@@ -302,11 +335,13 @@ class TestTools:
|
|
| 302 |
|
| 303 |
|
| 304 |
class TestResources:
|
| 305 |
-
async def test_list_resources(
|
|
|
|
|
|
|
| 306 |
"""
|
| 307 |
By default, resources exclude GET methods without parameters
|
| 308 |
"""
|
| 309 |
-
async with Client(
|
| 310 |
resources = await client.list_resources()
|
| 311 |
assert len(resources) == 4
|
| 312 |
assert resources[0].uri == AnyUrl("resource://get_users_users_get")
|
|
@@ -314,7 +349,7 @@ class TestResources:
|
|
| 314 |
|
| 315 |
async def test_get_resource(
|
| 316 |
self,
|
| 317 |
-
|
| 318 |
api_client,
|
| 319 |
users_db: dict[int, User],
|
| 320 |
):
|
|
@@ -325,7 +360,7 @@ class TestResources:
|
|
| 325 |
json_users = TypeAdapter(list[User]).dump_python(
|
| 326 |
sorted(users_db.values(), key=lambda x: x.id)
|
| 327 |
)
|
| 328 |
-
async with Client(
|
| 329 |
resource_response = await client.read_resource(
|
| 330 |
"resource://get_users_users_get"
|
| 331 |
)
|
|
@@ -337,11 +372,11 @@ class TestResources:
|
|
| 337 |
|
| 338 |
async def test_get_bytes_resource(
|
| 339 |
self,
|
| 340 |
-
|
| 341 |
api_client,
|
| 342 |
):
|
| 343 |
"""Test reading a resource that returns bytes."""
|
| 344 |
-
async with Client(
|
| 345 |
resource_response = await client.read_resource(
|
| 346 |
"resource://ping_bytes_ping_bytes_get"
|
| 347 |
)
|
|
@@ -350,23 +385,23 @@ class TestResources:
|
|
| 350 |
|
| 351 |
async def test_get_str_resource(
|
| 352 |
self,
|
| 353 |
-
|
| 354 |
api_client,
|
| 355 |
):
|
| 356 |
"""Test reading a resource that returns a string."""
|
| 357 |
-
async with Client(
|
| 358 |
resource_response = await client.read_resource("resource://ping_ping_get")
|
| 359 |
assert resource_response[0].text == "pong" # type: ignore[attr-defined]
|
| 360 |
|
| 361 |
|
| 362 |
class TestResourceTemplates:
|
| 363 |
async def test_list_resource_templates(
|
| 364 |
-
self,
|
| 365 |
):
|
| 366 |
"""
|
| 367 |
By default, resource templates exclude GET methods without parameters
|
| 368 |
"""
|
| 369 |
-
async with Client(
|
| 370 |
resource_templates = await client.list_resource_templates()
|
| 371 |
assert len(resource_templates) == 2
|
| 372 |
assert resource_templates[0].name == "get_user_users"
|
|
@@ -381,7 +416,7 @@ class TestResourceTemplates:
|
|
| 381 |
|
| 382 |
async def test_get_resource_template(
|
| 383 |
self,
|
| 384 |
-
|
| 385 |
api_client,
|
| 386 |
users_db: dict[int, User],
|
| 387 |
):
|
|
@@ -389,7 +424,7 @@ class TestResourceTemplates:
|
|
| 389 |
The resource template created by the OpenAPI server should be the same as the original
|
| 390 |
"""
|
| 391 |
user_id = 2
|
| 392 |
-
async with Client(
|
| 393 |
resource_response = await client.read_resource(
|
| 394 |
f"resource://get_user_users/{user_id}"
|
| 395 |
)
|
|
@@ -402,7 +437,7 @@ class TestResourceTemplates:
|
|
| 402 |
|
| 403 |
async def test_get_resource_template_multi_param(
|
| 404 |
self,
|
| 405 |
-
|
| 406 |
api_client,
|
| 407 |
users_db: dict[int, User],
|
| 408 |
):
|
|
@@ -411,7 +446,7 @@ class TestResourceTemplates:
|
|
| 411 |
"""
|
| 412 |
user_id = 2
|
| 413 |
is_active = True
|
| 414 |
-
async with Client(
|
| 415 |
resource_response = await client.read_resource(
|
| 416 |
f"resource://get_user_active_state_users/{is_active}/{user_id}"
|
| 417 |
)
|
|
@@ -424,11 +459,13 @@ class TestResourceTemplates:
|
|
| 424 |
|
| 425 |
|
| 426 |
class TestPrompts:
|
| 427 |
-
async def test_list_prompts(
|
|
|
|
|
|
|
| 428 |
"""
|
| 429 |
By default, there are no prompts.
|
| 430 |
"""
|
| 431 |
-
async with Client(
|
| 432 |
prompts = await client.list_prompts()
|
| 433 |
assert len(prompts) == 0
|
| 434 |
|
|
@@ -437,11 +474,11 @@ class TestTagTransfer:
|
|
| 437 |
"""Tests for transferring tags from OpenAPI routes to MCP objects."""
|
| 438 |
|
| 439 |
async def test_tags_transferred_to_tools(
|
| 440 |
-
self,
|
| 441 |
):
|
| 442 |
"""Test that tags from OpenAPI routes are correctly transferred to Tools."""
|
| 443 |
# Get internal tools directly (not the public API which returns MCP.Content)
|
| 444 |
-
tools =
|
| 445 |
|
| 446 |
# Find the create_user and update_user_name tools
|
| 447 |
create_user_tool = next(
|
|
@@ -465,12 +502,12 @@ class TestTagTransfer:
|
|
| 465 |
assert len(update_user_tool.tags) == 2
|
| 466 |
|
| 467 |
async def test_tags_transferred_to_resources(
|
| 468 |
-
self,
|
| 469 |
):
|
| 470 |
"""Test that tags from OpenAPI routes are correctly transferred to Resources."""
|
| 471 |
# Get internal resources directly
|
| 472 |
resources = list(
|
| 473 |
-
|
| 474 |
)
|
| 475 |
|
| 476 |
# Find the get_users resource
|
|
@@ -486,12 +523,12 @@ class TestTagTransfer:
|
|
| 486 |
assert len(get_users_resource.tags) == 2
|
| 487 |
|
| 488 |
async def test_tags_transferred_to_resource_templates(
|
| 489 |
-
self,
|
| 490 |
):
|
| 491 |
"""Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
|
| 492 |
# Get internal resource templates directly
|
| 493 |
templates = list(
|
| 494 |
-
|
| 495 |
)
|
| 496 |
|
| 497 |
# Find the get_user template
|
|
@@ -507,12 +544,12 @@ class TestTagTransfer:
|
|
| 507 |
assert len(get_user_template.tags) == 2
|
| 508 |
|
| 509 |
async def test_tags_preserved_in_resources_created_from_templates(
|
| 510 |
-
self,
|
| 511 |
):
|
| 512 |
"""Test that tags are preserved when creating resources from templates."""
|
| 513 |
# Get internal resource templates directly
|
| 514 |
templates = list(
|
| 515 |
-
|
| 516 |
)
|
| 517 |
|
| 518 |
# Find the get_user template
|
|
@@ -624,46 +661,49 @@ class TestOpenAPI30Compatibility:
|
|
| 624 |
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
| 625 |
|
| 626 |
@pytest.fixture
|
| 627 |
-
async def
|
| 628 |
self, openapi_30_spec, mock_30_client
|
| 629 |
) -> FastMCPOpenAPI:
|
| 630 |
"""Create a FastMCPOpenAPI server from the OpenAPI 3.0 spec."""
|
| 631 |
return FastMCPOpenAPI(
|
| 632 |
-
openapi_spec=openapi_30_spec,
|
|
|
|
|
|
|
|
|
|
| 633 |
)
|
| 634 |
|
| 635 |
-
async def test_server_creation(self,
|
| 636 |
"""Test that a server can be created from an OpenAPI 3.0 spec."""
|
| 637 |
-
assert isinstance(
|
| 638 |
-
assert
|
| 639 |
|
| 640 |
-
async def test_resource_discovery(self,
|
| 641 |
"""Test that resources are correctly discovered from an OpenAPI 3.0 spec."""
|
| 642 |
-
async with Client(
|
| 643 |
resources = await client.list_resources()
|
| 644 |
assert len(resources) == 1
|
| 645 |
assert resources[0].uri == AnyUrl("resource://listProducts")
|
| 646 |
|
| 647 |
-
async def test_resource_template_discovery(self,
|
| 648 |
"""Test that resource templates are correctly discovered from an OpenAPI 3.0 spec."""
|
| 649 |
-
async with Client(
|
| 650 |
templates = await client.list_resource_templates()
|
| 651 |
assert len(templates) == 1
|
| 652 |
assert templates[0].name == "getProduct"
|
| 653 |
assert templates[0].uriTemplate == r"resource://getProduct/{product_id}"
|
| 654 |
|
| 655 |
-
async def test_tool_discovery(self,
|
| 656 |
"""Test that tools are correctly discovered from an OpenAPI 3.0 spec."""
|
| 657 |
-
async with Client(
|
| 658 |
tools = await client.list_tools()
|
| 659 |
assert len(tools) == 1
|
| 660 |
assert tools[0].name == "createProduct"
|
| 661 |
assert "name" in tools[0].inputSchema["properties"]
|
| 662 |
assert "price" in tools[0].inputSchema["properties"]
|
| 663 |
|
| 664 |
-
async def test_resource_access(self,
|
| 665 |
"""Test reading a resource from an OpenAPI 3.0 server."""
|
| 666 |
-
async with Client(
|
| 667 |
resource_response = await client.read_resource("resource://listProducts")
|
| 668 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 669 |
content = json.loads(response_text)
|
|
@@ -671,9 +711,9 @@ class TestOpenAPI30Compatibility:
|
|
| 671 |
assert content[0]["name"] == "Product 1"
|
| 672 |
assert content[1]["name"] == "Product 2"
|
| 673 |
|
| 674 |
-
async def test_resource_template_access(self,
|
| 675 |
"""Test reading a resource from template from an OpenAPI 3.0 server."""
|
| 676 |
-
async with Client(
|
| 677 |
resource_response = await client.read_resource("resource://getProduct/p1")
|
| 678 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 679 |
content = json.loads(response_text)
|
|
@@ -681,9 +721,9 @@ class TestOpenAPI30Compatibility:
|
|
| 681 |
assert content["name"] == "Product 1"
|
| 682 |
assert content["price"] == 19.99
|
| 683 |
|
| 684 |
-
async def test_tool_execution(self,
|
| 685 |
"""Test executing a tool from an OpenAPI 3.0 server."""
|
| 686 |
-
async with Client(
|
| 687 |
result = await client.call_tool(
|
| 688 |
"createProduct", {"name": "New Product", "price": 39.99}
|
| 689 |
)
|
|
@@ -797,46 +837,49 @@ class TestOpenAPI31Compatibility:
|
|
| 797 |
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
| 798 |
|
| 799 |
@pytest.fixture
|
| 800 |
-
async def
|
| 801 |
self, openapi_31_spec, mock_31_client
|
| 802 |
) -> FastMCPOpenAPI:
|
| 803 |
"""Create a FastMCPOpenAPI server from the OpenAPI 3.1 spec."""
|
| 804 |
return FastMCPOpenAPI(
|
| 805 |
-
openapi_spec=openapi_31_spec,
|
|
|
|
|
|
|
|
|
|
| 806 |
)
|
| 807 |
|
| 808 |
-
async def test_server_creation(self,
|
| 809 |
"""Test that a server can be created from an OpenAPI 3.1 spec."""
|
| 810 |
-
assert isinstance(
|
| 811 |
-
assert
|
| 812 |
|
| 813 |
-
async def test_resource_discovery(self,
|
| 814 |
"""Test that resources are correctly discovered from an OpenAPI 3.1 spec."""
|
| 815 |
-
async with Client(
|
| 816 |
resources = await client.list_resources()
|
| 817 |
assert len(resources) == 1
|
| 818 |
assert resources[0].uri == AnyUrl("resource://listOrders")
|
| 819 |
|
| 820 |
-
async def test_resource_template_discovery(self,
|
| 821 |
"""Test that resource templates are correctly discovered from an OpenAPI 3.1 spec."""
|
| 822 |
-
async with Client(
|
| 823 |
templates = await client.list_resource_templates()
|
| 824 |
assert len(templates) == 1
|
| 825 |
assert templates[0].name == "getOrder"
|
| 826 |
assert templates[0].uriTemplate == r"resource://getOrder/{order_id}"
|
| 827 |
|
| 828 |
-
async def test_tool_discovery(self,
|
| 829 |
"""Test that tools are correctly discovered from an OpenAPI 3.1 spec."""
|
| 830 |
-
async with Client(
|
| 831 |
tools = await client.list_tools()
|
| 832 |
assert len(tools) == 1
|
| 833 |
assert tools[0].name == "createOrder"
|
| 834 |
assert "customer" in tools[0].inputSchema["properties"]
|
| 835 |
assert "items" in tools[0].inputSchema["properties"]
|
| 836 |
|
| 837 |
-
async def test_resource_access(self,
|
| 838 |
"""Test reading a resource from an OpenAPI 3.1 server."""
|
| 839 |
-
async with Client(
|
| 840 |
resource_response = await client.read_resource("resource://listOrders")
|
| 841 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 842 |
content = json.loads(response_text)
|
|
@@ -844,9 +887,9 @@ class TestOpenAPI31Compatibility:
|
|
| 844 |
assert content[0]["customer"] == "Alice"
|
| 845 |
assert content[1]["customer"] == "Bob"
|
| 846 |
|
| 847 |
-
async def test_resource_template_access(self,
|
| 848 |
"""Test reading a resource from template from an OpenAPI 3.1 server."""
|
| 849 |
-
async with Client(
|
| 850 |
resource_response = await client.read_resource("resource://getOrder/o1")
|
| 851 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 852 |
content = json.loads(response_text)
|
|
@@ -854,9 +897,9 @@ class TestOpenAPI31Compatibility:
|
|
| 854 |
assert content["customer"] == "Alice"
|
| 855 |
assert content["items"] == ["item1", "item2"]
|
| 856 |
|
| 857 |
-
async def test_tool_execution(self,
|
| 858 |
"""Test executing a tool from an OpenAPI 3.1 server."""
|
| 859 |
-
async with Client(
|
| 860 |
result = await client.call_tool(
|
| 861 |
"createOrder", {"customer": "Charlie", "items": ["item4", "item5"]}
|
| 862 |
)
|
|
@@ -871,11 +914,13 @@ class TestOpenAPI31Compatibility:
|
|
| 871 |
class TestMountFastMCP:
|
| 872 |
"""Tests for mounting FastMCP servers."""
|
| 873 |
|
| 874 |
-
async def test_mount_fastmcp(
|
|
|
|
|
|
|
| 875 |
"""Test mounting an OpenAPI server."""
|
| 876 |
mcp = FastMCP("MainApp")
|
| 877 |
|
| 878 |
-
await mcp.import_server("fastapi",
|
| 879 |
|
| 880 |
# Check that resources are available with prefixed URIs
|
| 881 |
async with Client(mcp) as client:
|
|
@@ -1144,19 +1189,24 @@ class TestDescriptionPropagation:
|
|
| 1144 |
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
| 1145 |
|
| 1146 |
@pytest.fixture
|
| 1147 |
-
async def
|
| 1148 |
"""Create a FastMCPOpenAPI server with the simple test spec."""
|
| 1149 |
return FastMCPOpenAPI(
|
| 1150 |
openapi_spec=simple_openapi_spec,
|
| 1151 |
client=mock_client,
|
| 1152 |
name="Test API",
|
|
|
|
| 1153 |
)
|
| 1154 |
|
| 1155 |
# --- RESOURCE TESTS ---
|
| 1156 |
|
| 1157 |
-
async def test_resource_includes_route_description(
|
|
|
|
|
|
|
| 1158 |
"""Test that a Resource includes the route description."""
|
| 1159 |
-
resources = list(
|
|
|
|
|
|
|
| 1160 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1161 |
|
| 1162 |
assert list_resource is not None, "listItems resource wasn't created"
|
|
@@ -1164,9 +1214,13 @@ class TestDescriptionPropagation:
|
|
| 1164 |
"Route description missing from Resource"
|
| 1165 |
)
|
| 1166 |
|
| 1167 |
-
async def test_resource_includes_response_description(
|
|
|
|
|
|
|
| 1168 |
"""Test that a Resource includes the response description."""
|
| 1169 |
-
resources = list(
|
|
|
|
|
|
|
| 1170 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1171 |
|
| 1172 |
assert list_resource is not None, "listItems resource wasn't created"
|
|
@@ -1174,9 +1228,13 @@ class TestDescriptionPropagation:
|
|
| 1174 |
"Response description missing from Resource"
|
| 1175 |
)
|
| 1176 |
|
| 1177 |
-
async def test_resource_includes_response_model_fields(
|
|
|
|
|
|
|
| 1178 |
"""Test that a Resource description includes response model field descriptions."""
|
| 1179 |
-
resources = list(
|
|
|
|
|
|
|
| 1180 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1181 |
|
| 1182 |
assert list_resource is not None, "listItems resource wasn't created"
|
|
@@ -1193,9 +1251,13 @@ class TestDescriptionPropagation:
|
|
| 1193 |
|
| 1194 |
# --- RESOURCE TEMPLATE TESTS ---
|
| 1195 |
|
| 1196 |
-
async def test_template_includes_route_description(
|
|
|
|
|
|
|
| 1197 |
"""Test that a ResourceTemplate includes the route description."""
|
| 1198 |
-
templates = list(
|
|
|
|
|
|
|
| 1199 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1200 |
|
| 1201 |
assert get_template is not None, "getItem template wasn't created"
|
|
@@ -1203,9 +1265,13 @@ class TestDescriptionPropagation:
|
|
| 1203 |
"Route description missing from ResourceTemplate"
|
| 1204 |
)
|
| 1205 |
|
| 1206 |
-
async def test_template_includes_function_docstring(
|
|
|
|
|
|
|
| 1207 |
"""Test that a ResourceTemplate includes the function docstring."""
|
| 1208 |
-
templates = list(
|
|
|
|
|
|
|
| 1209 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1210 |
|
| 1211 |
assert get_template is not None, "getItem template wasn't created"
|
|
@@ -1213,9 +1279,13 @@ class TestDescriptionPropagation:
|
|
| 1213 |
"Function docstring missing from ResourceTemplate"
|
| 1214 |
)
|
| 1215 |
|
| 1216 |
-
async def test_template_includes_path_parameter_description(
|
|
|
|
|
|
|
| 1217 |
"""Test that a ResourceTemplate includes path parameter descriptions."""
|
| 1218 |
-
templates = list(
|
|
|
|
|
|
|
| 1219 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1220 |
|
| 1221 |
assert get_template is not None, "getItem template wasn't created"
|
|
@@ -1223,9 +1293,13 @@ class TestDescriptionPropagation:
|
|
| 1223 |
"Path parameter description missing from ResourceTemplate description"
|
| 1224 |
)
|
| 1225 |
|
| 1226 |
-
async def test_template_includes_query_parameter_description(
|
|
|
|
|
|
|
| 1227 |
"""Test that a ResourceTemplate includes query parameter descriptions."""
|
| 1228 |
-
templates = list(
|
|
|
|
|
|
|
| 1229 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1230 |
|
| 1231 |
assert get_template is not None, "getItem template wasn't created"
|
|
@@ -1233,9 +1307,13 @@ class TestDescriptionPropagation:
|
|
| 1233 |
"Query parameter description missing from ResourceTemplate description"
|
| 1234 |
)
|
| 1235 |
|
| 1236 |
-
async def test_template_parameter_schema_includes_description(
|
|
|
|
|
|
|
| 1237 |
"""Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
|
| 1238 |
-
templates = list(
|
|
|
|
|
|
|
| 1239 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1240 |
|
| 1241 |
assert get_template is not None, "getItem template wasn't created"
|
|
@@ -1255,9 +1333,9 @@ class TestDescriptionPropagation:
|
|
| 1255 |
|
| 1256 |
# --- TOOL TESTS ---
|
| 1257 |
|
| 1258 |
-
async def test_tool_includes_route_description(self,
|
| 1259 |
"""Test that a Tool includes the route description."""
|
| 1260 |
-
tools =
|
| 1261 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1262 |
|
| 1263 |
assert create_tool is not None, "createItem tool wasn't created"
|
|
@@ -1265,9 +1343,9 @@ class TestDescriptionPropagation:
|
|
| 1265 |
"Route description missing from Tool"
|
| 1266 |
)
|
| 1267 |
|
| 1268 |
-
async def test_tool_includes_function_docstring(self,
|
| 1269 |
"""Test that a Tool includes the function docstring."""
|
| 1270 |
-
tools =
|
| 1271 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1272 |
|
| 1273 |
assert create_tool is not None, "createItem tool wasn't created"
|
|
@@ -1277,10 +1355,10 @@ class TestDescriptionPropagation:
|
|
| 1277 |
)
|
| 1278 |
|
| 1279 |
async def test_tool_parameter_schema_includes_property_description(
|
| 1280 |
-
self,
|
| 1281 |
):
|
| 1282 |
"""Test that a Tool's parameter schema includes property descriptions from request model."""
|
| 1283 |
-
tools =
|
| 1284 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1285 |
|
| 1286 |
assert create_tool is not None, "createItem tool wasn't created"
|
|
@@ -1300,9 +1378,9 @@ class TestDescriptionPropagation:
|
|
| 1300 |
|
| 1301 |
# --- CLIENT API TESTS ---
|
| 1302 |
|
| 1303 |
-
async def test_client_api_resource_description(self,
|
| 1304 |
"""Test that Resource descriptions are accessible via the client API."""
|
| 1305 |
-
async with Client(
|
| 1306 |
resources = await client.list_resources()
|
| 1307 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1308 |
|
|
@@ -1314,9 +1392,9 @@ class TestDescriptionPropagation:
|
|
| 1314 |
"Route description missing in Resource from client API"
|
| 1315 |
)
|
| 1316 |
|
| 1317 |
-
async def test_client_api_template_description(self,
|
| 1318 |
"""Test that ResourceTemplate descriptions are accessible via the client API."""
|
| 1319 |
-
async with Client(
|
| 1320 |
templates = await client.list_resource_templates()
|
| 1321 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1322 |
|
|
@@ -1328,9 +1406,9 @@ class TestDescriptionPropagation:
|
|
| 1328 |
"Route description missing in ResourceTemplate from client API"
|
| 1329 |
)
|
| 1330 |
|
| 1331 |
-
async def test_client_api_tool_description(self,
|
| 1332 |
"""Test that Tool descriptions are accessible via the client API."""
|
| 1333 |
-
async with Client(
|
| 1334 |
tools = await client.list_tools()
|
| 1335 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1336 |
|
|
@@ -1342,9 +1420,9 @@ class TestDescriptionPropagation:
|
|
| 1342 |
"Function docstring missing in Tool from client API"
|
| 1343 |
)
|
| 1344 |
|
| 1345 |
-
async def test_client_api_tool_parameter_schema(self,
|
| 1346 |
"""Test that Tool parameter schemas are accessible via the client API."""
|
| 1347 |
-
async with Client(
|
| 1348 |
tools = await client.list_tools()
|
| 1349 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1350 |
|
|
@@ -1699,9 +1777,11 @@ class TestFastAPIDescriptionPropagation:
|
|
| 1699 |
class TestReprMethods:
|
| 1700 |
"""Tests for the custom __repr__ methods of OpenAPI objects."""
|
| 1701 |
|
| 1702 |
-
async def test_openapi_tool_repr(
|
|
|
|
|
|
|
| 1703 |
"""Test that OpenAPITool's __repr__ method works without recursion errors."""
|
| 1704 |
-
tools =
|
| 1705 |
tool = next(iter(tools))
|
| 1706 |
|
| 1707 |
# Verify repr doesn't cause recursion and contains expected elements
|
|
@@ -1711,10 +1791,12 @@ class TestReprMethods:
|
|
| 1711 |
assert "method=" in tool_repr
|
| 1712 |
assert "path=" in tool_repr
|
| 1713 |
|
| 1714 |
-
async def test_openapi_resource_repr(
|
|
|
|
|
|
|
| 1715 |
"""Test that OpenAPIResource's __repr__ method works without recursion errors."""
|
| 1716 |
resources = list(
|
| 1717 |
-
|
| 1718 |
)
|
| 1719 |
resource = next(iter(resources))
|
| 1720 |
|
|
@@ -1726,11 +1808,11 @@ class TestReprMethods:
|
|
| 1726 |
assert "path=" in resource_repr
|
| 1727 |
|
| 1728 |
async def test_openapi_resource_template_repr(
|
| 1729 |
-
self,
|
| 1730 |
):
|
| 1731 |
"""Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
|
| 1732 |
templates = list(
|
| 1733 |
-
|
| 1734 |
)
|
| 1735 |
template = next(iter(templates))
|
| 1736 |
|
|
@@ -2182,6 +2264,7 @@ class TestMCPNames:
|
|
| 2182 |
openapi_spec=mcp_names_openapi_spec,
|
| 2183 |
client=mock_client,
|
| 2184 |
mcp_names=mcp_names,
|
|
|
|
| 2185 |
)
|
| 2186 |
|
| 2187 |
# Check tools use custom names
|
|
@@ -2212,6 +2295,7 @@ class TestMCPNames:
|
|
| 2212 |
openapi_spec=mcp_names_openapi_spec,
|
| 2213 |
client=mock_client,
|
| 2214 |
mcp_names=mcp_names,
|
|
|
|
| 2215 |
)
|
| 2216 |
|
| 2217 |
tools = server._tool_manager.list_tools()
|
|
@@ -2235,6 +2319,7 @@ class TestMCPNames:
|
|
| 2235 |
server = FastMCPOpenAPI(
|
| 2236 |
openapi_spec=mcp_names_openapi_spec,
|
| 2237 |
client=mock_client,
|
|
|
|
| 2238 |
)
|
| 2239 |
|
| 2240 |
resources = list(server._resource_manager.get_resources().values())
|
|
@@ -2261,6 +2346,7 @@ class TestMCPNames:
|
|
| 2261 |
server = FastMCPOpenAPI(
|
| 2262 |
openapi_spec=mcp_names_openapi_spec,
|
| 2263 |
client=mock_client,
|
|
|
|
| 2264 |
)
|
| 2265 |
|
| 2266 |
# Check all component types
|
|
@@ -2299,9 +2385,9 @@ class TestMCPNames:
|
|
| 2299 |
mcp_names=mcp_names,
|
| 2300 |
)
|
| 2301 |
|
| 2302 |
-
|
| 2303 |
-
|
| 2304 |
-
assert "openapi_user_list" in
|
| 2305 |
|
| 2306 |
async def test_mcp_names_with_from_fastapi_classmethod(self):
|
| 2307 |
"""Test mcp_names works with FastMCP.from_fastapi() classmethod."""
|
|
@@ -2334,11 +2420,8 @@ class TestMCPNames:
|
|
| 2334 |
tools = server._tool_manager.list_tools()
|
| 2335 |
tool_names = {tool.name for tool in tools}
|
| 2336 |
|
| 2337 |
-
resources = list(server._resource_manager.get_resources().values())
|
| 2338 |
-
resource_names = {resource.name for resource in resources}
|
| 2339 |
-
|
| 2340 |
assert "fastapi_create_user" in tool_names
|
| 2341 |
-
assert "fastapi_user_list" in
|
| 2342 |
|
| 2343 |
async def test_mcp_names_custom_names_are_also_truncated(
|
| 2344 |
self, mcp_names_openapi_spec, mock_client
|
|
@@ -2355,6 +2438,7 @@ class TestMCPNames:
|
|
| 2355 |
openapi_spec=mcp_names_openapi_spec,
|
| 2356 |
client=mock_client,
|
| 2357 |
mcp_names=mcp_names,
|
|
|
|
| 2358 |
)
|
| 2359 |
|
| 2360 |
resources = list(server._resource_manager.get_resources().values())
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
|
| 49 |
+
# route maps for GET requests
|
| 50 |
+
# use these to create components of all types instead of just tools
|
| 51 |
+
GET_ROUTE_MAPS = [
|
| 52 |
+
# GET requests with path parameters go to ResourceTemplate
|
| 53 |
+
RouteMap(
|
| 54 |
+
methods=["GET"],
|
| 55 |
+
pattern=r".*\{.*\}.*",
|
| 56 |
+
mcp_type=MCPType.RESOURCE_TEMPLATE,
|
| 57 |
+
),
|
| 58 |
+
# GET requests without path parameters go to Resource
|
| 59 |
+
RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
@pytest.fixture
|
| 64 |
def fastapi_app(users_db: dict[int, User]) -> FastAPI:
|
| 65 |
app = FastAPI(title="FastAPI App")
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
@pytest.fixture
|
| 139 |
+
async def fastmcp_openapi_server_with_all_types(
|
| 140 |
fastapi_app: FastAPI, api_client: httpx.AsyncClient
|
| 141 |
) -> FastMCPOpenAPI:
|
| 142 |
openapi_spec = fastapi_app.openapi()
|
|
|
|
| 145 |
openapi_spec=openapi_spec,
|
| 146 |
client=api_client,
|
| 147 |
name="Test App",
|
| 148 |
+
route_maps=GET_ROUTE_MAPS,
|
| 149 |
)
|
| 150 |
|
| 151 |
|
|
|
|
| 184 |
client=api_client,
|
| 185 |
name="Test App",
|
| 186 |
timeout=1.0,
|
| 187 |
+
route_maps=GET_ROUTE_MAPS,
|
| 188 |
)
|
| 189 |
assert server._timeout == 1.0
|
| 190 |
|
|
|
|
| 202 |
|
| 203 |
|
| 204 |
class TestTools:
|
| 205 |
+
async def test_default_behavior_converts_everything_to_tools(
|
| 206 |
+
self, fastapi_app: FastAPI
|
| 207 |
+
):
|
| 208 |
+
"""
|
| 209 |
+
By default, tools exclude GET methods
|
| 210 |
+
"""
|
| 211 |
+
server = FastMCPOpenAPI.from_fastapi(fastapi_app)
|
| 212 |
+
assert len(await server.get_tools()) == 8
|
| 213 |
+
assert len(await server.get_resources()) == 0
|
| 214 |
+
assert len(await server.get_resource_templates()) == 0
|
| 215 |
+
|
| 216 |
+
async def test_list_tools(
|
| 217 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 218 |
+
):
|
| 219 |
"""
|
| 220 |
By default, tools exclude GET methods
|
| 221 |
"""
|
| 222 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 223 |
tools = await client.list_tools()
|
| 224 |
assert len(tools) == 2
|
| 225 |
|
|
|
|
| 253 |
)
|
| 254 |
|
| 255 |
async def test_call_create_user_tool(
|
| 256 |
+
self,
|
| 257 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 258 |
+
api_client,
|
| 259 |
):
|
| 260 |
"""
|
| 261 |
The tool created by the OpenAPI server should be the same as the original
|
| 262 |
"""
|
| 263 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 264 |
tool_response = await client.call_tool(
|
| 265 |
"create_user_users_post", {"name": "David", "active": False}
|
| 266 |
)
|
|
|
|
| 274 |
assert len(response.json()) == 4
|
| 275 |
|
| 276 |
# Check that the user was created via MCP
|
| 277 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 278 |
user_response = await client.read_resource("resource://get_user_users/4")
|
| 279 |
response_text = user_response[0].text # type: ignore[attr-defined]
|
| 280 |
user = json.loads(response_text)
|
| 281 |
assert user == expected_user
|
| 282 |
|
| 283 |
async def test_call_update_user_name_tool(
|
| 284 |
+
self,
|
| 285 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 286 |
+
api_client,
|
| 287 |
):
|
| 288 |
"""
|
| 289 |
The tool created by the OpenAPI server should be the same as the original
|
| 290 |
"""
|
| 291 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 292 |
tool_response = await client.call_tool(
|
| 293 |
"update_user_name_users",
|
| 294 |
{"user_id": 1, "name": "XYZ"},
|
|
|
|
| 303 |
assert expected_data in response.json()
|
| 304 |
|
| 305 |
# Check that the user was updated via MCP
|
| 306 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 307 |
user_response = await client.read_resource("resource://get_user_users/1")
|
| 308 |
response_text = user_response[0].text # type: ignore[attr-defined]
|
| 309 |
user = json.loads(response_text)
|
|
|
|
| 335 |
|
| 336 |
|
| 337 |
class TestResources:
|
| 338 |
+
async def test_list_resources(
|
| 339 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 340 |
+
):
|
| 341 |
"""
|
| 342 |
By default, resources exclude GET methods without parameters
|
| 343 |
"""
|
| 344 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 345 |
resources = await client.list_resources()
|
| 346 |
assert len(resources) == 4
|
| 347 |
assert resources[0].uri == AnyUrl("resource://get_users_users_get")
|
|
|
|
| 349 |
|
| 350 |
async def test_get_resource(
|
| 351 |
self,
|
| 352 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 353 |
api_client,
|
| 354 |
users_db: dict[int, User],
|
| 355 |
):
|
|
|
|
| 360 |
json_users = TypeAdapter(list[User]).dump_python(
|
| 361 |
sorted(users_db.values(), key=lambda x: x.id)
|
| 362 |
)
|
| 363 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 364 |
resource_response = await client.read_resource(
|
| 365 |
"resource://get_users_users_get"
|
| 366 |
)
|
|
|
|
| 372 |
|
| 373 |
async def test_get_bytes_resource(
|
| 374 |
self,
|
| 375 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 376 |
api_client,
|
| 377 |
):
|
| 378 |
"""Test reading a resource that returns bytes."""
|
| 379 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 380 |
resource_response = await client.read_resource(
|
| 381 |
"resource://ping_bytes_ping_bytes_get"
|
| 382 |
)
|
|
|
|
| 385 |
|
| 386 |
async def test_get_str_resource(
|
| 387 |
self,
|
| 388 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 389 |
api_client,
|
| 390 |
):
|
| 391 |
"""Test reading a resource that returns a string."""
|
| 392 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 393 |
resource_response = await client.read_resource("resource://ping_ping_get")
|
| 394 |
assert resource_response[0].text == "pong" # type: ignore[attr-defined]
|
| 395 |
|
| 396 |
|
| 397 |
class TestResourceTemplates:
|
| 398 |
async def test_list_resource_templates(
|
| 399 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 400 |
):
|
| 401 |
"""
|
| 402 |
By default, resource templates exclude GET methods without parameters
|
| 403 |
"""
|
| 404 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 405 |
resource_templates = await client.list_resource_templates()
|
| 406 |
assert len(resource_templates) == 2
|
| 407 |
assert resource_templates[0].name == "get_user_users"
|
|
|
|
| 416 |
|
| 417 |
async def test_get_resource_template(
|
| 418 |
self,
|
| 419 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 420 |
api_client,
|
| 421 |
users_db: dict[int, User],
|
| 422 |
):
|
|
|
|
| 424 |
The resource template created by the OpenAPI server should be the same as the original
|
| 425 |
"""
|
| 426 |
user_id = 2
|
| 427 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 428 |
resource_response = await client.read_resource(
|
| 429 |
f"resource://get_user_users/{user_id}"
|
| 430 |
)
|
|
|
|
| 437 |
|
| 438 |
async def test_get_resource_template_multi_param(
|
| 439 |
self,
|
| 440 |
+
fastmcp_openapi_server_with_all_types: FastMCPOpenAPI,
|
| 441 |
api_client,
|
| 442 |
users_db: dict[int, User],
|
| 443 |
):
|
|
|
|
| 446 |
"""
|
| 447 |
user_id = 2
|
| 448 |
is_active = True
|
| 449 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 450 |
resource_response = await client.read_resource(
|
| 451 |
f"resource://get_user_active_state_users/{is_active}/{user_id}"
|
| 452 |
)
|
|
|
|
| 459 |
|
| 460 |
|
| 461 |
class TestPrompts:
|
| 462 |
+
async def test_list_prompts(
|
| 463 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 464 |
+
):
|
| 465 |
"""
|
| 466 |
By default, there are no prompts.
|
| 467 |
"""
|
| 468 |
+
async with Client(fastmcp_openapi_server_with_all_types) as client:
|
| 469 |
prompts = await client.list_prompts()
|
| 470 |
assert len(prompts) == 0
|
| 471 |
|
|
|
|
| 474 |
"""Tests for transferring tags from OpenAPI routes to MCP objects."""
|
| 475 |
|
| 476 |
async def test_tags_transferred_to_tools(
|
| 477 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 478 |
):
|
| 479 |
"""Test that tags from OpenAPI routes are correctly transferred to Tools."""
|
| 480 |
# Get internal tools directly (not the public API which returns MCP.Content)
|
| 481 |
+
tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools()
|
| 482 |
|
| 483 |
# Find the create_user and update_user_name tools
|
| 484 |
create_user_tool = next(
|
|
|
|
| 502 |
assert len(update_user_tool.tags) == 2
|
| 503 |
|
| 504 |
async def test_tags_transferred_to_resources(
|
| 505 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 506 |
):
|
| 507 |
"""Test that tags from OpenAPI routes are correctly transferred to Resources."""
|
| 508 |
# Get internal resources directly
|
| 509 |
resources = list(
|
| 510 |
+
fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values()
|
| 511 |
)
|
| 512 |
|
| 513 |
# Find the get_users resource
|
|
|
|
| 523 |
assert len(get_users_resource.tags) == 2
|
| 524 |
|
| 525 |
async def test_tags_transferred_to_resource_templates(
|
| 526 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 527 |
):
|
| 528 |
"""Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
|
| 529 |
# Get internal resource templates directly
|
| 530 |
templates = list(
|
| 531 |
+
fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
|
| 532 |
)
|
| 533 |
|
| 534 |
# Find the get_user template
|
|
|
|
| 544 |
assert len(get_user_template.tags) == 2
|
| 545 |
|
| 546 |
async def test_tags_preserved_in_resources_created_from_templates(
|
| 547 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 548 |
):
|
| 549 |
"""Test that tags are preserved when creating resources from templates."""
|
| 550 |
# Get internal resource templates directly
|
| 551 |
templates = list(
|
| 552 |
+
fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
|
| 553 |
)
|
| 554 |
|
| 555 |
# Find the get_user template
|
|
|
|
| 661 |
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
| 662 |
|
| 663 |
@pytest.fixture
|
| 664 |
+
async def openapi_30_server_with_all_types(
|
| 665 |
self, openapi_30_spec, mock_30_client
|
| 666 |
) -> FastMCPOpenAPI:
|
| 667 |
"""Create a FastMCPOpenAPI server from the OpenAPI 3.0 spec."""
|
| 668 |
return FastMCPOpenAPI(
|
| 669 |
+
openapi_spec=openapi_30_spec,
|
| 670 |
+
client=mock_30_client,
|
| 671 |
+
name="Product API 3.0",
|
| 672 |
+
route_maps=GET_ROUTE_MAPS,
|
| 673 |
)
|
| 674 |
|
| 675 |
+
async def test_server_creation(self, openapi_30_server_with_all_types):
|
| 676 |
"""Test that a server can be created from an OpenAPI 3.0 spec."""
|
| 677 |
+
assert isinstance(openapi_30_server_with_all_types, FastMCP)
|
| 678 |
+
assert openapi_30_server_with_all_types.name == "Product API 3.0"
|
| 679 |
|
| 680 |
+
async def test_resource_discovery(self, openapi_30_server_with_all_types):
|
| 681 |
"""Test that resources are correctly discovered from an OpenAPI 3.0 spec."""
|
| 682 |
+
async with Client(openapi_30_server_with_all_types) as client:
|
| 683 |
resources = await client.list_resources()
|
| 684 |
assert len(resources) == 1
|
| 685 |
assert resources[0].uri == AnyUrl("resource://listProducts")
|
| 686 |
|
| 687 |
+
async def test_resource_template_discovery(self, openapi_30_server_with_all_types):
|
| 688 |
"""Test that resource templates are correctly discovered from an OpenAPI 3.0 spec."""
|
| 689 |
+
async with Client(openapi_30_server_with_all_types) as client:
|
| 690 |
templates = await client.list_resource_templates()
|
| 691 |
assert len(templates) == 1
|
| 692 |
assert templates[0].name == "getProduct"
|
| 693 |
assert templates[0].uriTemplate == r"resource://getProduct/{product_id}"
|
| 694 |
|
| 695 |
+
async def test_tool_discovery(self, openapi_30_server_with_all_types):
|
| 696 |
"""Test that tools are correctly discovered from an OpenAPI 3.0 spec."""
|
| 697 |
+
async with Client(openapi_30_server_with_all_types) as client:
|
| 698 |
tools = await client.list_tools()
|
| 699 |
assert len(tools) == 1
|
| 700 |
assert tools[0].name == "createProduct"
|
| 701 |
assert "name" in tools[0].inputSchema["properties"]
|
| 702 |
assert "price" in tools[0].inputSchema["properties"]
|
| 703 |
|
| 704 |
+
async def test_resource_access(self, openapi_30_server_with_all_types):
|
| 705 |
"""Test reading a resource from an OpenAPI 3.0 server."""
|
| 706 |
+
async with Client(openapi_30_server_with_all_types) as client:
|
| 707 |
resource_response = await client.read_resource("resource://listProducts")
|
| 708 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 709 |
content = json.loads(response_text)
|
|
|
|
| 711 |
assert content[0]["name"] == "Product 1"
|
| 712 |
assert content[1]["name"] == "Product 2"
|
| 713 |
|
| 714 |
+
async def test_resource_template_access(self, openapi_30_server_with_all_types):
|
| 715 |
"""Test reading a resource from template from an OpenAPI 3.0 server."""
|
| 716 |
+
async with Client(openapi_30_server_with_all_types) as client:
|
| 717 |
resource_response = await client.read_resource("resource://getProduct/p1")
|
| 718 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 719 |
content = json.loads(response_text)
|
|
|
|
| 721 |
assert content["name"] == "Product 1"
|
| 722 |
assert content["price"] == 19.99
|
| 723 |
|
| 724 |
+
async def test_tool_execution(self, openapi_30_server_with_all_types):
|
| 725 |
"""Test executing a tool from an OpenAPI 3.0 server."""
|
| 726 |
+
async with Client(openapi_30_server_with_all_types) as client:
|
| 727 |
result = await client.call_tool(
|
| 728 |
"createProduct", {"name": "New Product", "price": 39.99}
|
| 729 |
)
|
|
|
|
| 837 |
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
| 838 |
|
| 839 |
@pytest.fixture
|
| 840 |
+
async def openapi_31_server_with_all_types(
|
| 841 |
self, openapi_31_spec, mock_31_client
|
| 842 |
) -> FastMCPOpenAPI:
|
| 843 |
"""Create a FastMCPOpenAPI server from the OpenAPI 3.1 spec."""
|
| 844 |
return FastMCPOpenAPI(
|
| 845 |
+
openapi_spec=openapi_31_spec,
|
| 846 |
+
client=mock_31_client,
|
| 847 |
+
name="Order API 3.1",
|
| 848 |
+
route_maps=GET_ROUTE_MAPS,
|
| 849 |
)
|
| 850 |
|
| 851 |
+
async def test_server_creation(self, openapi_31_server_with_all_types):
|
| 852 |
"""Test that a server can be created from an OpenAPI 3.1 spec."""
|
| 853 |
+
assert isinstance(openapi_31_server_with_all_types, FastMCP)
|
| 854 |
+
assert openapi_31_server_with_all_types.name == "Order API 3.1"
|
| 855 |
|
| 856 |
+
async def test_resource_discovery(self, openapi_31_server_with_all_types):
|
| 857 |
"""Test that resources are correctly discovered from an OpenAPI 3.1 spec."""
|
| 858 |
+
async with Client(openapi_31_server_with_all_types) as client:
|
| 859 |
resources = await client.list_resources()
|
| 860 |
assert len(resources) == 1
|
| 861 |
assert resources[0].uri == AnyUrl("resource://listOrders")
|
| 862 |
|
| 863 |
+
async def test_resource_template_discovery(self, openapi_31_server_with_all_types):
|
| 864 |
"""Test that resource templates are correctly discovered from an OpenAPI 3.1 spec."""
|
| 865 |
+
async with Client(openapi_31_server_with_all_types) as client:
|
| 866 |
templates = await client.list_resource_templates()
|
| 867 |
assert len(templates) == 1
|
| 868 |
assert templates[0].name == "getOrder"
|
| 869 |
assert templates[0].uriTemplate == r"resource://getOrder/{order_id}"
|
| 870 |
|
| 871 |
+
async def test_tool_discovery(self, openapi_31_server_with_all_types):
|
| 872 |
"""Test that tools are correctly discovered from an OpenAPI 3.1 spec."""
|
| 873 |
+
async with Client(openapi_31_server_with_all_types) as client:
|
| 874 |
tools = await client.list_tools()
|
| 875 |
assert len(tools) == 1
|
| 876 |
assert tools[0].name == "createOrder"
|
| 877 |
assert "customer" in tools[0].inputSchema["properties"]
|
| 878 |
assert "items" in tools[0].inputSchema["properties"]
|
| 879 |
|
| 880 |
+
async def test_resource_access(self, openapi_31_server_with_all_types):
|
| 881 |
"""Test reading a resource from an OpenAPI 3.1 server."""
|
| 882 |
+
async with Client(openapi_31_server_with_all_types) as client:
|
| 883 |
resource_response = await client.read_resource("resource://listOrders")
|
| 884 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 885 |
content = json.loads(response_text)
|
|
|
|
| 887 |
assert content[0]["customer"] == "Alice"
|
| 888 |
assert content[1]["customer"] == "Bob"
|
| 889 |
|
| 890 |
+
async def test_resource_template_access(self, openapi_31_server_with_all_types):
|
| 891 |
"""Test reading a resource from template from an OpenAPI 3.1 server."""
|
| 892 |
+
async with Client(openapi_31_server_with_all_types) as client:
|
| 893 |
resource_response = await client.read_resource("resource://getOrder/o1")
|
| 894 |
response_text = resource_response[0].text # type: ignore[attr-defined]
|
| 895 |
content = json.loads(response_text)
|
|
|
|
| 897 |
assert content["customer"] == "Alice"
|
| 898 |
assert content["items"] == ["item1", "item2"]
|
| 899 |
|
| 900 |
+
async def test_tool_execution(self, openapi_31_server_with_all_types):
|
| 901 |
"""Test executing a tool from an OpenAPI 3.1 server."""
|
| 902 |
+
async with Client(openapi_31_server_with_all_types) as client:
|
| 903 |
result = await client.call_tool(
|
| 904 |
"createOrder", {"customer": "Charlie", "items": ["item4", "item5"]}
|
| 905 |
)
|
|
|
|
| 914 |
class TestMountFastMCP:
|
| 915 |
"""Tests for mounting FastMCP servers."""
|
| 916 |
|
| 917 |
+
async def test_mount_fastmcp(
|
| 918 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 919 |
+
):
|
| 920 |
"""Test mounting an OpenAPI server."""
|
| 921 |
mcp = FastMCP("MainApp")
|
| 922 |
|
| 923 |
+
await mcp.import_server("fastapi", fastmcp_openapi_server_with_all_types)
|
| 924 |
|
| 925 |
# Check that resources are available with prefixed URIs
|
| 926 |
async with Client(mcp) as client:
|
|
|
|
| 1189 |
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
| 1190 |
|
| 1191 |
@pytest.fixture
|
| 1192 |
+
async def simple_server_with_all_types(self, simple_openapi_spec, mock_client):
|
| 1193 |
"""Create a FastMCPOpenAPI server with the simple test spec."""
|
| 1194 |
return FastMCPOpenAPI(
|
| 1195 |
openapi_spec=simple_openapi_spec,
|
| 1196 |
client=mock_client,
|
| 1197 |
name="Test API",
|
| 1198 |
+
route_maps=GET_ROUTE_MAPS,
|
| 1199 |
)
|
| 1200 |
|
| 1201 |
# --- RESOURCE TESTS ---
|
| 1202 |
|
| 1203 |
+
async def test_resource_includes_route_description(
|
| 1204 |
+
self, simple_server_with_all_types
|
| 1205 |
+
):
|
| 1206 |
"""Test that a Resource includes the route description."""
|
| 1207 |
+
resources = list(
|
| 1208 |
+
simple_server_with_all_types._resource_manager.get_resources().values()
|
| 1209 |
+
)
|
| 1210 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1211 |
|
| 1212 |
assert list_resource is not None, "listItems resource wasn't created"
|
|
|
|
| 1214 |
"Route description missing from Resource"
|
| 1215 |
)
|
| 1216 |
|
| 1217 |
+
async def test_resource_includes_response_description(
|
| 1218 |
+
self, simple_server_with_all_types
|
| 1219 |
+
):
|
| 1220 |
"""Test that a Resource includes the response description."""
|
| 1221 |
+
resources = list(
|
| 1222 |
+
simple_server_with_all_types._resource_manager.get_resources().values()
|
| 1223 |
+
)
|
| 1224 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1225 |
|
| 1226 |
assert list_resource is not None, "listItems resource wasn't created"
|
|
|
|
| 1228 |
"Response description missing from Resource"
|
| 1229 |
)
|
| 1230 |
|
| 1231 |
+
async def test_resource_includes_response_model_fields(
|
| 1232 |
+
self, simple_server_with_all_types
|
| 1233 |
+
):
|
| 1234 |
"""Test that a Resource description includes response model field descriptions."""
|
| 1235 |
+
resources = list(
|
| 1236 |
+
simple_server_with_all_types._resource_manager.get_resources().values()
|
| 1237 |
+
)
|
| 1238 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1239 |
|
| 1240 |
assert list_resource is not None, "listItems resource wasn't created"
|
|
|
|
| 1251 |
|
| 1252 |
# --- RESOURCE TEMPLATE TESTS ---
|
| 1253 |
|
| 1254 |
+
async def test_template_includes_route_description(
|
| 1255 |
+
self, simple_server_with_all_types
|
| 1256 |
+
):
|
| 1257 |
"""Test that a ResourceTemplate includes the route description."""
|
| 1258 |
+
templates = list(
|
| 1259 |
+
simple_server_with_all_types._resource_manager.get_templates().values()
|
| 1260 |
+
)
|
| 1261 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1262 |
|
| 1263 |
assert get_template is not None, "getItem template wasn't created"
|
|
|
|
| 1265 |
"Route description missing from ResourceTemplate"
|
| 1266 |
)
|
| 1267 |
|
| 1268 |
+
async def test_template_includes_function_docstring(
|
| 1269 |
+
self, simple_server_with_all_types
|
| 1270 |
+
):
|
| 1271 |
"""Test that a ResourceTemplate includes the function docstring."""
|
| 1272 |
+
templates = list(
|
| 1273 |
+
simple_server_with_all_types._resource_manager.get_templates().values()
|
| 1274 |
+
)
|
| 1275 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1276 |
|
| 1277 |
assert get_template is not None, "getItem template wasn't created"
|
|
|
|
| 1279 |
"Function docstring missing from ResourceTemplate"
|
| 1280 |
)
|
| 1281 |
|
| 1282 |
+
async def test_template_includes_path_parameter_description(
|
| 1283 |
+
self, simple_server_with_all_types
|
| 1284 |
+
):
|
| 1285 |
"""Test that a ResourceTemplate includes path parameter descriptions."""
|
| 1286 |
+
templates = list(
|
| 1287 |
+
simple_server_with_all_types._resource_manager.get_templates().values()
|
| 1288 |
+
)
|
| 1289 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1290 |
|
| 1291 |
assert get_template is not None, "getItem template wasn't created"
|
|
|
|
| 1293 |
"Path parameter description missing from ResourceTemplate description"
|
| 1294 |
)
|
| 1295 |
|
| 1296 |
+
async def test_template_includes_query_parameter_description(
|
| 1297 |
+
self, simple_server_with_all_types
|
| 1298 |
+
):
|
| 1299 |
"""Test that a ResourceTemplate includes query parameter descriptions."""
|
| 1300 |
+
templates = list(
|
| 1301 |
+
simple_server_with_all_types._resource_manager.get_templates().values()
|
| 1302 |
+
)
|
| 1303 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1304 |
|
| 1305 |
assert get_template is not None, "getItem template wasn't created"
|
|
|
|
| 1307 |
"Query parameter description missing from ResourceTemplate description"
|
| 1308 |
)
|
| 1309 |
|
| 1310 |
+
async def test_template_parameter_schema_includes_description(
|
| 1311 |
+
self, simple_server_with_all_types
|
| 1312 |
+
):
|
| 1313 |
"""Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
|
| 1314 |
+
templates = list(
|
| 1315 |
+
simple_server_with_all_types._resource_manager.get_templates().values()
|
| 1316 |
+
)
|
| 1317 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1318 |
|
| 1319 |
assert get_template is not None, "getItem template wasn't created"
|
|
|
|
| 1333 |
|
| 1334 |
# --- TOOL TESTS ---
|
| 1335 |
|
| 1336 |
+
async def test_tool_includes_route_description(self, simple_server_with_all_types):
|
| 1337 |
"""Test that a Tool includes the route description."""
|
| 1338 |
+
tools = simple_server_with_all_types._tool_manager.list_tools()
|
| 1339 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1340 |
|
| 1341 |
assert create_tool is not None, "createItem tool wasn't created"
|
|
|
|
| 1343 |
"Route description missing from Tool"
|
| 1344 |
)
|
| 1345 |
|
| 1346 |
+
async def test_tool_includes_function_docstring(self, simple_server_with_all_types):
|
| 1347 |
"""Test that a Tool includes the function docstring."""
|
| 1348 |
+
tools = simple_server_with_all_types._tool_manager.list_tools()
|
| 1349 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1350 |
|
| 1351 |
assert create_tool is not None, "createItem tool wasn't created"
|
|
|
|
| 1355 |
)
|
| 1356 |
|
| 1357 |
async def test_tool_parameter_schema_includes_property_description(
|
| 1358 |
+
self, simple_server_with_all_types
|
| 1359 |
):
|
| 1360 |
"""Test that a Tool's parameter schema includes property descriptions from request model."""
|
| 1361 |
+
tools = simple_server_with_all_types._tool_manager.list_tools()
|
| 1362 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1363 |
|
| 1364 |
assert create_tool is not None, "createItem tool wasn't created"
|
|
|
|
| 1378 |
|
| 1379 |
# --- CLIENT API TESTS ---
|
| 1380 |
|
| 1381 |
+
async def test_client_api_resource_description(self, simple_server_with_all_types):
|
| 1382 |
"""Test that Resource descriptions are accessible via the client API."""
|
| 1383 |
+
async with Client(simple_server_with_all_types) as client:
|
| 1384 |
resources = await client.list_resources()
|
| 1385 |
list_resource = next((r for r in resources if r.name == "listItems"), None)
|
| 1386 |
|
|
|
|
| 1392 |
"Route description missing in Resource from client API"
|
| 1393 |
)
|
| 1394 |
|
| 1395 |
+
async def test_client_api_template_description(self, simple_server_with_all_types):
|
| 1396 |
"""Test that ResourceTemplate descriptions are accessible via the client API."""
|
| 1397 |
+
async with Client(simple_server_with_all_types) as client:
|
| 1398 |
templates = await client.list_resource_templates()
|
| 1399 |
get_template = next((t for t in templates if t.name == "getItem"), None)
|
| 1400 |
|
|
|
|
| 1406 |
"Route description missing in ResourceTemplate from client API"
|
| 1407 |
)
|
| 1408 |
|
| 1409 |
+
async def test_client_api_tool_description(self, simple_server_with_all_types):
|
| 1410 |
"""Test that Tool descriptions are accessible via the client API."""
|
| 1411 |
+
async with Client(simple_server_with_all_types) as client:
|
| 1412 |
tools = await client.list_tools()
|
| 1413 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1414 |
|
|
|
|
| 1420 |
"Function docstring missing in Tool from client API"
|
| 1421 |
)
|
| 1422 |
|
| 1423 |
+
async def test_client_api_tool_parameter_schema(self, simple_server_with_all_types):
|
| 1424 |
"""Test that Tool parameter schemas are accessible via the client API."""
|
| 1425 |
+
async with Client(simple_server_with_all_types) as client:
|
| 1426 |
tools = await client.list_tools()
|
| 1427 |
create_tool = next((t for t in tools if t.name == "createItem"), None)
|
| 1428 |
|
|
|
|
| 1777 |
class TestReprMethods:
|
| 1778 |
"""Tests for the custom __repr__ methods of OpenAPI objects."""
|
| 1779 |
|
| 1780 |
+
async def test_openapi_tool_repr(
|
| 1781 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 1782 |
+
):
|
| 1783 |
"""Test that OpenAPITool's __repr__ method works without recursion errors."""
|
| 1784 |
+
tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools()
|
| 1785 |
tool = next(iter(tools))
|
| 1786 |
|
| 1787 |
# Verify repr doesn't cause recursion and contains expected elements
|
|
|
|
| 1791 |
assert "method=" in tool_repr
|
| 1792 |
assert "path=" in tool_repr
|
| 1793 |
|
| 1794 |
+
async def test_openapi_resource_repr(
|
| 1795 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 1796 |
+
):
|
| 1797 |
"""Test that OpenAPIResource's __repr__ method works without recursion errors."""
|
| 1798 |
resources = list(
|
| 1799 |
+
fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values()
|
| 1800 |
)
|
| 1801 |
resource = next(iter(resources))
|
| 1802 |
|
|
|
|
| 1808 |
assert "path=" in resource_repr
|
| 1809 |
|
| 1810 |
async def test_openapi_resource_template_repr(
|
| 1811 |
+
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 1812 |
):
|
| 1813 |
"""Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
|
| 1814 |
templates = list(
|
| 1815 |
+
fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
|
| 1816 |
)
|
| 1817 |
template = next(iter(templates))
|
| 1818 |
|
|
|
|
| 2264 |
openapi_spec=mcp_names_openapi_spec,
|
| 2265 |
client=mock_client,
|
| 2266 |
mcp_names=mcp_names,
|
| 2267 |
+
route_maps=GET_ROUTE_MAPS,
|
| 2268 |
)
|
| 2269 |
|
| 2270 |
# Check tools use custom names
|
|
|
|
| 2295 |
openapi_spec=mcp_names_openapi_spec,
|
| 2296 |
client=mock_client,
|
| 2297 |
mcp_names=mcp_names,
|
| 2298 |
+
route_maps=GET_ROUTE_MAPS,
|
| 2299 |
)
|
| 2300 |
|
| 2301 |
tools = server._tool_manager.list_tools()
|
|
|
|
| 2319 |
server = FastMCPOpenAPI(
|
| 2320 |
openapi_spec=mcp_names_openapi_spec,
|
| 2321 |
client=mock_client,
|
| 2322 |
+
route_maps=GET_ROUTE_MAPS,
|
| 2323 |
)
|
| 2324 |
|
| 2325 |
resources = list(server._resource_manager.get_resources().values())
|
|
|
|
| 2346 |
server = FastMCPOpenAPI(
|
| 2347 |
openapi_spec=mcp_names_openapi_spec,
|
| 2348 |
client=mock_client,
|
| 2349 |
+
route_maps=GET_ROUTE_MAPS,
|
| 2350 |
)
|
| 2351 |
|
| 2352 |
# Check all component types
|
|
|
|
| 2385 |
mcp_names=mcp_names,
|
| 2386 |
)
|
| 2387 |
|
| 2388 |
+
tools = server._tool_manager.list_tools()
|
| 2389 |
+
tool_names = {tool.name for tool in tools}
|
| 2390 |
+
assert "openapi_user_list" in tool_names
|
| 2391 |
|
| 2392 |
async def test_mcp_names_with_from_fastapi_classmethod(self):
|
| 2393 |
"""Test mcp_names works with FastMCP.from_fastapi() classmethod."""
|
|
|
|
| 2420 |
tools = server._tool_manager.list_tools()
|
| 2421 |
tool_names = {tool.name for tool in tools}
|
| 2422 |
|
|
|
|
|
|
|
|
|
|
| 2423 |
assert "fastapi_create_user" in tool_names
|
| 2424 |
+
assert "fastapi_user_list" in tool_names
|
| 2425 |
|
| 2426 |
async def test_mcp_names_custom_names_are_also_truncated(
|
| 2427 |
self, mcp_names_openapi_spec, mock_client
|
|
|
|
| 2438 |
openapi_spec=mcp_names_openapi_spec,
|
| 2439 |
client=mock_client,
|
| 2440 |
mcp_names=mcp_names,
|
| 2441 |
+
route_maps=GET_ROUTE_MAPS,
|
| 2442 |
)
|
| 2443 |
|
| 2444 |
resources = list(server._resource_manager.get_resources().values())
|
tests/server/openapi/test_route_map_fn.py
CHANGED
|
@@ -161,15 +161,15 @@ def test_route_map_fn_returns_none(sample_openapi_spec, http_client):
|
|
| 161 |
|
| 162 |
# Should have default behavior
|
| 163 |
assert server.name == "Test Server"
|
| 164 |
-
# Check that components were created with default
|
| 165 |
tools = server._tool_manager._tools
|
| 166 |
resources = server._resource_manager._resources
|
| 167 |
templates = server._resource_manager._templates
|
| 168 |
|
| 169 |
# Should have tools, resources, and templates based on default mapping
|
| 170 |
assert len(tools) > 0
|
| 171 |
-
assert len(resources)
|
| 172 |
-
assert len(templates)
|
| 173 |
|
| 174 |
|
| 175 |
def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_client):
|
|
|
|
| 161 |
|
| 162 |
# Should have default behavior
|
| 163 |
assert server.name == "Test Server"
|
| 164 |
+
# Check that components were created with default mapping
|
| 165 |
tools = server._tool_manager._tools
|
| 166 |
resources = server._resource_manager._resources
|
| 167 |
templates = server._resource_manager._templates
|
| 168 |
|
| 169 |
# Should have tools, resources, and templates based on default mapping
|
| 170 |
assert len(tools) > 0
|
| 171 |
+
assert len(resources) == 0
|
| 172 |
+
assert len(templates) == 0
|
| 173 |
|
| 174 |
|
| 175 |
def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_client):
|