Jeremiah Lowin commited on
Commit
4476591
·
unverified ·
2 Parent(s): f563519996e149

Merge pull request #788 from jlowin/openapi

Browse files
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
- To illustrate this in practice, here are FastMCP's default rules as a list of `RouteMap` objects:
64
 
65
  ```python
66
  from fastmcp.server.openapi import RouteMap, MCPType
67
 
68
  DEFAULT_ROUTE_MAPPINGS = [
69
-
70
- # GET with path parameters → ResourceTemplate
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, the following simple rule will treat every OpenAPI route as a tool:
98
 
99
- ```python {7}
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 mappings as a list, where order determines priority
 
159
  DEFAULT_ROUTE_MAPPINGS = [
160
- # GET requests with path parameters go to ResourceTemplate
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, MCPType, RouteMap
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, MCPType, RouteMap
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 fastmcp_openapi_server(
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 test_list_tools(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  """
191
  By default, tools exclude GET methods
192
  """
193
- async with Client(fastmcp_openapi_server) as 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, fastmcp_openapi_server: FastMCPOpenAPI, api_client
 
 
228
  ):
229
  """
230
  The tool created by the OpenAPI server should be the same as the original
231
  """
232
- async with Client(fastmcp_openapi_server) as 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(fastmcp_openapi_server) as 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, fastmcp_openapi_server: FastMCPOpenAPI, api_client
 
 
254
  ):
255
  """
256
  The tool created by the OpenAPI server should be the same as the original
257
  """
258
- async with Client(fastmcp_openapi_server) as 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(fastmcp_openapi_server) as 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(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
306
  """
307
  By default, resources exclude GET methods without parameters
308
  """
309
- async with Client(fastmcp_openapi_server) as 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
- fastmcp_openapi_server: FastMCPOpenAPI,
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(fastmcp_openapi_server) as 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
- fastmcp_openapi_server: FastMCPOpenAPI,
341
  api_client,
342
  ):
343
  """Test reading a resource that returns bytes."""
344
- async with Client(fastmcp_openapi_server) as 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
- fastmcp_openapi_server: FastMCPOpenAPI,
354
  api_client,
355
  ):
356
  """Test reading a resource that returns a string."""
357
- async with Client(fastmcp_openapi_server) as 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, fastmcp_openapi_server: FastMCPOpenAPI
365
  ):
366
  """
367
  By default, resource templates exclude GET methods without parameters
368
  """
369
- async with Client(fastmcp_openapi_server) as 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
- fastmcp_openapi_server: FastMCPOpenAPI,
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(fastmcp_openapi_server) as 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
- fastmcp_openapi_server: FastMCPOpenAPI,
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(fastmcp_openapi_server) as 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(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
428
  """
429
  By default, there are no prompts.
430
  """
431
- async with Client(fastmcp_openapi_server) as 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, fastmcp_openapi_server: FastMCPOpenAPI
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 = fastmcp_openapi_server._tool_manager.list_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, fastmcp_openapi_server: FastMCPOpenAPI
469
  ):
470
  """Test that tags from OpenAPI routes are correctly transferred to Resources."""
471
  # Get internal resources directly
472
  resources = list(
473
- fastmcp_openapi_server._resource_manager.get_resources().values()
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, fastmcp_openapi_server: FastMCPOpenAPI
490
  ):
491
  """Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
492
  # Get internal resource templates directly
493
  templates = list(
494
- fastmcp_openapi_server._resource_manager.get_templates().values()
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, fastmcp_openapi_server: FastMCPOpenAPI
511
  ):
512
  """Test that tags are preserved when creating resources from templates."""
513
  # Get internal resource templates directly
514
  templates = list(
515
- fastmcp_openapi_server._resource_manager.get_templates().values()
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 openapi_30_server(
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, client=mock_30_client, name="Product API 3.0"
 
 
 
633
  )
634
 
635
- async def test_server_creation(self, openapi_30_server):
636
  """Test that a server can be created from an OpenAPI 3.0 spec."""
637
- assert isinstance(openapi_30_server, FastMCP)
638
- assert openapi_30_server.name == "Product API 3.0"
639
 
640
- async def test_resource_discovery(self, openapi_30_server):
641
  """Test that resources are correctly discovered from an OpenAPI 3.0 spec."""
642
- async with Client(openapi_30_server) as 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, openapi_30_server):
648
  """Test that resource templates are correctly discovered from an OpenAPI 3.0 spec."""
649
- async with Client(openapi_30_server) as 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, openapi_30_server):
656
  """Test that tools are correctly discovered from an OpenAPI 3.0 spec."""
657
- async with Client(openapi_30_server) as 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, openapi_30_server):
665
  """Test reading a resource from an OpenAPI 3.0 server."""
666
- async with Client(openapi_30_server) as 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, openapi_30_server):
675
  """Test reading a resource from template from an OpenAPI 3.0 server."""
676
- async with Client(openapi_30_server) as 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, openapi_30_server):
685
  """Test executing a tool from an OpenAPI 3.0 server."""
686
- async with Client(openapi_30_server) as 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 openapi_31_server(
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, client=mock_31_client, name="Order API 3.1"
 
 
 
806
  )
807
 
808
- async def test_server_creation(self, openapi_31_server):
809
  """Test that a server can be created from an OpenAPI 3.1 spec."""
810
- assert isinstance(openapi_31_server, FastMCP)
811
- assert openapi_31_server.name == "Order API 3.1"
812
 
813
- async def test_resource_discovery(self, openapi_31_server):
814
  """Test that resources are correctly discovered from an OpenAPI 3.1 spec."""
815
- async with Client(openapi_31_server) as 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, openapi_31_server):
821
  """Test that resource templates are correctly discovered from an OpenAPI 3.1 spec."""
822
- async with Client(openapi_31_server) as 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, openapi_31_server):
829
  """Test that tools are correctly discovered from an OpenAPI 3.1 spec."""
830
- async with Client(openapi_31_server) as 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, openapi_31_server):
838
  """Test reading a resource from an OpenAPI 3.1 server."""
839
- async with Client(openapi_31_server) as 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, openapi_31_server):
848
  """Test reading a resource from template from an OpenAPI 3.1 server."""
849
- async with Client(openapi_31_server) as 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, openapi_31_server):
858
  """Test executing a tool from an OpenAPI 3.1 server."""
859
- async with Client(openapi_31_server) as 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(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
875
  """Test mounting an OpenAPI server."""
876
  mcp = FastMCP("MainApp")
877
 
878
- await mcp.import_server("fastapi", fastmcp_openapi_server)
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 test_server(self, simple_openapi_spec, mock_client):
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(self, test_server):
 
 
1158
  """Test that a Resource includes the route description."""
1159
- resources = list(test_server._resource_manager.get_resources().values())
 
 
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(self, test_server):
 
 
1168
  """Test that a Resource includes the response description."""
1169
- resources = list(test_server._resource_manager.get_resources().values())
 
 
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(self, test_server):
 
 
1178
  """Test that a Resource description includes response model field descriptions."""
1179
- resources = list(test_server._resource_manager.get_resources().values())
 
 
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(self, test_server):
 
 
1197
  """Test that a ResourceTemplate includes the route description."""
1198
- templates = list(test_server._resource_manager.get_templates().values())
 
 
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(self, test_server):
 
 
1207
  """Test that a ResourceTemplate includes the function docstring."""
1208
- templates = list(test_server._resource_manager.get_templates().values())
 
 
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(self, test_server):
 
 
1217
  """Test that a ResourceTemplate includes path parameter descriptions."""
1218
- templates = list(test_server._resource_manager.get_templates().values())
 
 
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(self, test_server):
 
 
1227
  """Test that a ResourceTemplate includes query parameter descriptions."""
1228
- templates = list(test_server._resource_manager.get_templates().values())
 
 
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(self, test_server):
 
 
1237
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1238
- templates = list(test_server._resource_manager.get_templates().values())
 
 
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, test_server):
1259
  """Test that a Tool includes the route description."""
1260
- tools = test_server._tool_manager.list_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, test_server):
1269
  """Test that a Tool includes the function docstring."""
1270
- tools = test_server._tool_manager.list_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, test_server
1281
  ):
1282
  """Test that a Tool's parameter schema includes property descriptions from request model."""
1283
- tools = test_server._tool_manager.list_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, test_server):
1304
  """Test that Resource descriptions are accessible via the client API."""
1305
- async with Client(test_server) as 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, test_server):
1318
  """Test that ResourceTemplate descriptions are accessible via the client API."""
1319
- async with Client(test_server) as 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, test_server):
1332
  """Test that Tool descriptions are accessible via the client API."""
1333
- async with Client(test_server) as 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, test_server):
1346
  """Test that Tool parameter schemas are accessible via the client API."""
1347
- async with Client(test_server) as 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(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
1703
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1704
- tools = fastmcp_openapi_server._tool_manager.list_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(self, fastmcp_openapi_server: FastMCPOpenAPI):
 
 
1715
  """Test that OpenAPIResource's __repr__ method works without recursion errors."""
1716
  resources = list(
1717
- fastmcp_openapi_server._resource_manager.get_resources().values()
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, fastmcp_openapi_server: FastMCPOpenAPI
1730
  ):
1731
  """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
1732
  templates = list(
1733
- fastmcp_openapi_server._resource_manager.get_templates().values()
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
- resources = list(server._resource_manager.get_resources().values())
2303
- resource_names = {resource.name for resource in resources}
2304
- assert "openapi_user_list" in resource_names
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 resource_names
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 types
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):
 
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):