Jeremiah Lowin commited on
Commit
a1fa3c7
·
unverified ·
2 Parent(s): dc18c2c2be71a8

Merge pull request #578 from jlowin/headers

Browse files

Permit more flexible name generation for OpenAPI servers

docs/servers/openapi.mdx CHANGED
@@ -231,6 +231,38 @@ mcp = FastMCP.from_openapi(
231
 
232
  ## Customizing MCP Components
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  <VersionBadge version="2.5.0" />
235
 
236
  By default, FastMCP creates MCP components using a variety of metadata from the OpenAPI spec, such as incorporating the OpenAPI description into the MCP component description.
@@ -271,7 +303,6 @@ mcp = FastMCP.from_openapi(
271
  mcp_component_fn=customize_components,
272
  )
273
  ```
274
-
275
  ## Request Parameter Handling
276
 
277
  FastMCP intelligently handles different types of parameters in OpenAPI requests:
@@ -376,15 +407,15 @@ from fastmcp import FastMCP
376
  # Your FastAPI app
377
  app = FastAPI(title="My API", version="1.0.0")
378
 
379
- @app.get("/items", tags=["items"])
380
  def list_items():
381
  return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
382
 
383
- @app.get("/items/{item_id}", tags=["items", "detail"])
384
  def get_item(item_id: int):
385
  return {"id": item_id, "name": f"Item {item_id}"}
386
 
387
- @app.post("/items", tags=["items", "create"])
388
  def create_item(name: str):
389
  return {"id": 3, "name": name}
390
 
@@ -395,6 +426,8 @@ if __name__ == "__main__":
395
  mcp.run() # Run as MCP server
396
  ```
397
 
 
 
398
  <Warning>
399
  FastMCP servers are not FastAPI apps, even when created from one. To learn how to deploy them as an ASGI app, see the [ASGI Integration](/deployment/asgi) documentation.
400
  </Warning>
@@ -413,6 +446,7 @@ mcp = FastMCP.from_fastapi(
413
  app=app,
414
  name="My Custom Server",
415
  timeout=5.0,
 
416
  route_maps=[
417
  # Admin endpoints become tools
418
  RouteMap(methods="*", pattern=r"^/admin/.*", mcp_type=MCPType.TOOL),
@@ -421,6 +455,9 @@ mcp = FastMCP.from_fastapi(
421
  ],
422
  route_map_fn=my_route_mapper,
423
  mcp_component_fn=my_component_customizer,
 
 
 
424
  )
425
  ```
426
 
@@ -430,4 +467,3 @@ mcp = FastMCP.from_fastapi(
430
  - **Schema inheritance**: Pydantic models and validation are preserved
431
  - **ASGI transport**: Direct in-memory communication (no HTTP overhead)
432
  - **Full FastAPI features**: Dependencies, middleware, authentication all work
433
-
 
231
 
232
  ## Customizing MCP Components
233
 
234
+
235
+
236
+ ### Component Names
237
+
238
+ <VersionBadge version="2.5.0" />
239
+
240
+ FastMCP automatically generates names for MCP components based on the OpenAPI specification. By default, it uses the `operationId` from your OpenAPI spec, up to the first double underscore (`__`).
241
+
242
+ All component names are automatically:
243
+ - **Slugified**: Spaces and special characters are converted to underscores or removed
244
+ - **Truncated**: Limited to 56 characters maximum to ensure compatibility
245
+ - **Unique**: If multiple components have the same name, a number is automatically appended to make them unique
246
+
247
+ For more control over component names, you can provide an `mcp_names` dictionary that maps `operationId` values to your desired names. The `operationId` must be exactly as it appears in the OpenAPI spec. The provided name will always be slugified and truncated.
248
+
249
+ ```python {5-9}
250
+ from fastmcp import FastMCP
251
+
252
+ mcp = FastMCP.from_openapi(
253
+ ...
254
+ mcp_names={
255
+ "list_users__with_pagination": "user_list",
256
+ "create_user__admin_required": "create_user",
257
+ "get_user_details__admin_required": "user_detail",
258
+ }
259
+ )
260
+ ```
261
+
262
+ Any `operationId` not found in `mcp_names` will use the default strategy (operationId up to the first `__`).
263
+
264
+
265
+ ### Advanced Customization
266
  <VersionBadge version="2.5.0" />
267
 
268
  By default, FastMCP creates MCP components using a variety of metadata from the OpenAPI spec, such as incorporating the OpenAPI description into the MCP component description.
 
303
  mcp_component_fn=customize_components,
304
  )
305
  ```
 
306
  ## Request Parameter Handling
307
 
308
  FastMCP intelligently handles different types of parameters in OpenAPI requests:
 
407
  # Your FastAPI app
408
  app = FastAPI(title="My API", version="1.0.0")
409
 
410
+ @app.get("/items", tags=["items"], operation_id="list_items")
411
  def list_items():
412
  return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
413
 
414
+ @app.get("/items/{item_id}", tags=["items", "detail"], operation_id="get_item")
415
  def get_item(item_id: int):
416
  return {"id": item_id, "name": f"Item {item_id}"}
417
 
418
+ @app.post("/items", tags=["items", "create"], operation_id="create_item")
419
  def create_item(name: str):
420
  return {"id": 3, "name": name}
421
 
 
426
  mcp.run() # Run as MCP server
427
  ```
428
 
429
+ Note that operation ids are optional, but are used to create component names. You can also provide custom names, just like with OpenAPI specs.
430
+
431
  <Warning>
432
  FastMCP servers are not FastAPI apps, even when created from one. To learn how to deploy them as an ASGI app, see the [ASGI Integration](/deployment/asgi) documentation.
433
  </Warning>
 
446
  app=app,
447
  name="My Custom Server",
448
  timeout=5.0,
449
+ mcp_names={"operationId": "friendly_name"}, # Custom component names
450
  route_maps=[
451
  # Admin endpoints become tools
452
  RouteMap(methods="*", pattern=r"^/admin/.*", mcp_type=MCPType.TOOL),
 
455
  ],
456
  route_map_fn=my_route_mapper,
457
  mcp_component_fn=my_component_customizer,
458
+ mcp_names={
459
+ "get_user_details_users__user_id__get": "get_user_details",
460
+ }
461
  )
462
  ```
463
 
 
467
  - **Schema inheritance**: Pydantic models and validation are preserved
468
  - **ASGI transport**: Direct in-memory communication (no HTTP overhead)
469
  - **Full FastAPI features**: Dependencies, middleware, authentication all work
 
src/fastmcp/server/openapi.py CHANGED
@@ -6,6 +6,7 @@ import enum
6
  import json
7
  import re
8
  import warnings
 
9
  from collections.abc import Callable
10
  from dataclasses import dataclass, field
11
  from re import Pattern
@@ -36,6 +37,29 @@ logger = get_logger(__name__)
36
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
37
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  def _get_mcp_client_headers() -> dict[str, str]:
40
  """
41
  Extract headers from the current MCP client HTTP request if available.
@@ -695,6 +719,7 @@ class FastMCPOpenAPI(FastMCP):
695
  route_maps: list[RouteMap] | None = None,
696
  route_map_fn: RouteMapFn | None = None,
697
  mcp_component_fn: ComponentFn | None = None,
 
698
  timeout: float | None = None,
699
  **settings: Any,
700
  ):
@@ -712,6 +737,11 @@ class FastMCPOpenAPI(FastMCP):
712
  mcp_component_fn: Optional callable for component customization.
713
  Receives (route, component) and can modify the component in-place.
714
  Called on every created component.
 
 
 
 
 
715
  timeout: Optional timeout (in seconds) for all requests
716
  **settings: Additional settings for FastMCP
717
  """
@@ -721,9 +751,15 @@ class FastMCPOpenAPI(FastMCP):
721
  self._timeout = timeout
722
  self._route_map_fn = route_map_fn
723
  self._mcp_component_fn = mcp_component_fn
 
724
 
725
  # Keep track of names to detect collisions
726
- self._used_names = {"tools": set(), "resources": set(), "templates": set()}
 
 
 
 
 
727
 
728
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
729
 
@@ -766,40 +802,31 @@ class FastMCPOpenAPI(FastMCP):
766
  def _generate_default_name(
767
  self, route: openapi.HTTPRoute, mcp_type: MCPType
768
  ) -> str:
769
- """Generate a default name from the route path."""
770
- # First check for OpenAPI operationId which takes precedence
771
 
 
772
  if route.operation_id:
773
- return route.operation_id
774
-
775
- # For path-based naming, clean up the path
776
- path_parts = route.path.strip("/").split("/")
777
-
778
- # Remove path parameters (parts with {})
779
- clean_parts = []
780
- for part in path_parts:
781
- if part.startswith("{") and part.endswith("}"):
782
- # For templates, include parameter name without braces
783
- if mcp_type == MCPType.RESOURCE_TEMPLATE:
784
- param_name = part[1:-1] # Remove braces
785
- clean_parts.append(param_name)
786
  else:
787
- clean_parts.append(part)
 
 
 
788
 
789
- # Join the parts
790
- resource_name = "_".join(clean_parts)
791
 
792
- # For tools, might be useful to keep the method for clarity on what it does
793
- if mcp_type == MCPType.TOOL:
794
- # Only include method if it helps distinguish (POST, PUT, PATCH, DELETE)
795
- # For GET we don't need the method as it's implied for resources
796
- if route.method != "GET":
797
- resource_name = f"{route.method.lower()}_{resource_name}"
798
 
799
- return resource_name
800
 
801
  def _get_unique_name(
802
- self, name: str, component_type: Literal["tools", "resources", "templates"]
 
 
803
  ) -> str:
804
  """
805
  Ensure the name is unique within its component type by appending numbers if needed.
@@ -812,23 +839,18 @@ class FastMCPOpenAPI(FastMCP):
812
  str: A unique name for the component
813
  """
814
  # Check if the name is already used
815
- if name not in self._used_names[component_type]:
816
- self._used_names[component_type].add(name)
817
  return name
818
 
819
- # Find the next available number suffix
820
- counter = 2
821
- while f"{name}_{counter}" in self._used_names[component_type]:
822
- counter += 1
823
-
824
- # Create the new name
825
- new_name = f"{name}_{counter}"
826
- logger.debug(
827
- f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
828
- f"Using '{new_name}' instead."
829
- )
830
 
831
- self._used_names[component_type].add(new_name)
832
  return new_name
833
 
834
  def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
@@ -836,7 +858,7 @@ class FastMCPOpenAPI(FastMCP):
836
  combined_schema = _combine_schemas(route)
837
 
838
  # Get a unique tool name
839
- tool_name = self._get_unique_name(name, "tools")
840
 
841
  base_description = (
842
  route.description
@@ -882,7 +904,7 @@ class FastMCPOpenAPI(FastMCP):
882
  def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
883
  """Creates and registers an OpenAPIResource with enhanced description."""
884
  # Get a unique resource name
885
- resource_name = self._get_unique_name(name, "resources")
886
 
887
  resource_uri = f"resource://{resource_name}"
888
  base_description = (
@@ -927,7 +949,7 @@ class FastMCPOpenAPI(FastMCP):
927
  def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
928
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
929
  # Get a unique template name
930
- template_name = self._get_unique_name(name, "templates")
931
 
932
  path_params = [p.name for p in route.parameters if p.location == "path"]
933
  path_params.sort() # Sort for consistent URIs
 
6
  import json
7
  import re
8
  import warnings
9
+ from collections import Counter
10
  from collections.abc import Callable
11
  from dataclasses import dataclass, field
12
  from re import Pattern
 
37
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
38
 
39
 
40
+ def _slugify(text: str) -> str:
41
+ """
42
+ Convert text to a URL-friendly slug format that only contains lowercase
43
+ letters, uppercase letters, numbers, and underscores.
44
+ """
45
+ if not text:
46
+ return ""
47
+
48
+ # Replace spaces and common separators with underscores
49
+ slug = re.sub(r"[\s\-\.]+", "_", text)
50
+
51
+ # Remove non-alphanumeric characters except underscores
52
+ slug = re.sub(r"[^a-zA-Z0-9_]", "", slug)
53
+
54
+ # Remove multiple consecutive underscores
55
+ slug = re.sub(r"_+", "_", slug)
56
+
57
+ # Remove leading/trailing underscores
58
+ slug = slug.strip("_")
59
+
60
+ return slug
61
+
62
+
63
  def _get_mcp_client_headers() -> dict[str, str]:
64
  """
65
  Extract headers from the current MCP client HTTP request if available.
 
719
  route_maps: list[RouteMap] | None = None,
720
  route_map_fn: RouteMapFn | None = None,
721
  mcp_component_fn: ComponentFn | None = None,
722
+ mcp_names: dict[str, str] | None = None,
723
  timeout: float | None = None,
724
  **settings: Any,
725
  ):
 
737
  mcp_component_fn: Optional callable for component customization.
738
  Receives (route, component) and can modify the component in-place.
739
  Called on every created component.
740
+ mcp_names: Optional dictionary mapping operationId to desired component names.
741
+ If an operationId is not in the dictionary, falls back to using the
742
+ operationId up to the first double underscore. If no operationId exists,
743
+ falls back to slugified summary or path-based naming.
744
+ All names are truncated to 56 characters maximum.
745
  timeout: Optional timeout (in seconds) for all requests
746
  **settings: Additional settings for FastMCP
747
  """
 
751
  self._timeout = timeout
752
  self._route_map_fn = route_map_fn
753
  self._mcp_component_fn = mcp_component_fn
754
+ self._mcp_names = mcp_names or {}
755
 
756
  # Keep track of names to detect collisions
757
+ self._used_names = {
758
+ "tool": Counter(),
759
+ "resource": Counter(),
760
+ "resource_template": Counter(),
761
+ "prompt": Counter(),
762
+ }
763
 
764
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
765
 
 
802
  def _generate_default_name(
803
  self, route: openapi.HTTPRoute, mcp_type: MCPType
804
  ) -> str:
805
+ """Generate a default name from the route using the configured strategy."""
806
+ name = ""
807
 
808
+ # First check if there's a custom mapping for this operationId
809
  if route.operation_id:
810
+ if route.operation_id in self._mcp_names:
811
+ name = self._mcp_names[route.operation_id]
 
 
 
 
 
 
 
 
 
 
 
812
  else:
813
+ # If there's a double underscore in the operationId, use the first part
814
+ name = route.operation_id.split("__")[0]
815
+ else:
816
+ name = route.summary or f"{route.method}_{route.path}"
817
 
818
+ name = _slugify(name)
 
819
 
820
+ # Truncate to 56 characters maximum
821
+ if len(name) > 56:
822
+ name = name[:56]
 
 
 
823
 
824
+ return name
825
 
826
  def _get_unique_name(
827
+ self,
828
+ name: str,
829
+ component_type: Literal["tool", "resource", "resource_template", "prompt"],
830
  ) -> str:
831
  """
832
  Ensure the name is unique within its component type by appending numbers if needed.
 
839
  str: A unique name for the component
840
  """
841
  # Check if the name is already used
842
+ self._used_names[component_type][name] += 1
843
+ if self._used_names[component_type][name] == 1:
844
  return name
845
 
846
+ else:
847
+ # Create the new name
848
+ new_name = f"{name}_{self._used_names[component_type][name]}"
849
+ logger.debug(
850
+ f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
851
+ f"Using '{new_name}' instead."
852
+ )
 
 
 
 
853
 
 
854
  return new_name
855
 
856
  def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
 
858
  combined_schema = _combine_schemas(route)
859
 
860
  # Get a unique tool name
861
+ tool_name = self._get_unique_name(name, "tool")
862
 
863
  base_description = (
864
  route.description
 
904
  def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
905
  """Creates and registers an OpenAPIResource with enhanced description."""
906
  # Get a unique resource name
907
+ resource_name = self._get_unique_name(name, "resource")
908
 
909
  resource_uri = f"resource://{resource_name}"
910
  base_description = (
 
949
  def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
950
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
951
  # Get a unique template name
952
+ template_name = self._get_unique_name(name, "resource_template")
953
 
954
  path_params = [p.name for p in route.parameters if p.location == "path"]
955
  path_params.sort() # Sort for consistent URIs
src/fastmcp/server/server.py CHANGED
@@ -1170,6 +1170,7 @@ class FastMCP(Generic[LifespanResultT]):
1170
  route_maps: list[RouteMap] | None = None,
1171
  route_map_fn: OpenAPIRouteMapFn | None = None,
1172
  mcp_component_fn: OpenAPIComponentFn | None = None,
 
1173
  all_routes_as_tools: bool = False,
1174
  **settings: Any,
1175
  ) -> FastMCPOpenAPI:
@@ -1199,6 +1200,7 @@ class FastMCP(Generic[LifespanResultT]):
1199
  route_maps=route_maps,
1200
  route_map_fn=route_map_fn,
1201
  mcp_component_fn=mcp_component_fn,
 
1202
  **settings,
1203
  )
1204
 
@@ -1210,6 +1212,7 @@ class FastMCP(Generic[LifespanResultT]):
1210
  route_maps: list[RouteMap] | None = None,
1211
  route_map_fn: OpenAPIRouteMapFn | None = None,
1212
  mcp_component_fn: OpenAPIComponentFn | None = None,
 
1213
  all_routes_as_tools: bool = False,
1214
  httpx_client_kwargs: dict[str, Any] | None = None,
1215
  **settings: Any,
@@ -1253,6 +1256,7 @@ class FastMCP(Generic[LifespanResultT]):
1253
  route_maps=route_maps,
1254
  route_map_fn=route_map_fn,
1255
  mcp_component_fn=mcp_component_fn,
 
1256
  **settings,
1257
  )
1258
 
 
1170
  route_maps: list[RouteMap] | None = None,
1171
  route_map_fn: OpenAPIRouteMapFn | None = None,
1172
  mcp_component_fn: OpenAPIComponentFn | None = None,
1173
+ mcp_names: dict[str, str] | None = None,
1174
  all_routes_as_tools: bool = False,
1175
  **settings: Any,
1176
  ) -> FastMCPOpenAPI:
 
1200
  route_maps=route_maps,
1201
  route_map_fn=route_map_fn,
1202
  mcp_component_fn=mcp_component_fn,
1203
+ mcp_names=mcp_names,
1204
  **settings,
1205
  )
1206
 
 
1212
  route_maps: list[RouteMap] | None = None,
1213
  route_map_fn: OpenAPIRouteMapFn | None = None,
1214
  mcp_component_fn: OpenAPIComponentFn | None = None,
1215
+ mcp_names: dict[str, str] | None = None,
1216
  all_routes_as_tools: bool = False,
1217
  httpx_client_kwargs: dict[str, Any] | None = None,
1218
  **settings: Any,
 
1256
  route_maps=route_maps,
1257
  route_map_fn=route_map_fn,
1258
  mcp_component_fn=mcp_component_fn,
1259
+ mcp_names=mcp_names,
1260
  **settings,
1261
  )
1262
 
tests/client/test_openapi.py CHANGED
@@ -130,7 +130,7 @@ class TestClientHeaders:
130
  transport=SSETransport(sse_server, headers={"X-TEST": "test-123"})
131
  ) as client:
132
  result = await client.read_resource(
133
- "resource://get_header_by_name_headers__header_name__get/x-test"
134
  )
135
  assert isinstance(result[0], TextResourceContents)
136
  header = json.loads(result[0].text)
@@ -143,7 +143,7 @@ class TestClientHeaders:
143
  )
144
  ) as client:
145
  result = await client.read_resource(
146
- "resource://get_header_by_name_headers__header_name__get/x-test"
147
  )
148
  assert isinstance(result[0], TextResourceContents)
149
  header = json.loads(result[0].text)
 
130
  transport=SSETransport(sse_server, headers={"X-TEST": "test-123"})
131
  ) as client:
132
  result = await client.read_resource(
133
+ "resource://get_header_by_name_headers/x-test"
134
  )
135
  assert isinstance(result[0], TextResourceContents)
136
  header = json.loads(result[0].text)
 
143
  )
144
  ) as client:
145
  result = await client.read_resource(
146
+ "resource://get_header_by_name_headers/x-test"
147
  )
148
  assert isinstance(result[0], TextResourceContents)
149
  header = json.loads(result[0].text)
tests/server/openapi/test_openapi.py CHANGED
@@ -208,7 +208,7 @@ class TestTools:
208
  },
209
  )
210
  assert tools[1].model_dump() == dict(
211
- name="update_user_name_users__user_id__name_patch",
212
  annotations=None,
213
  description=IsStr(
214
  regex=r"^Update a user's name\..*$", regex_flags=re.DOTALL
@@ -248,9 +248,7 @@ class TestTools:
248
 
249
  # Check that the user was created via MCP
250
  async with Client(fastmcp_openapi_server) as client:
251
- user_response = await client.read_resource(
252
- "resource://get_user_users__user_id__get/4"
253
- )
254
  assert isinstance(user_response[0], TextResourceContents)
255
  response_text = user_response[0].text
256
  user = json.loads(response_text)
@@ -264,7 +262,7 @@ class TestTools:
264
  """
265
  async with Client(fastmcp_openapi_server) as client:
266
  tool_response = await client.call_tool(
267
- "update_user_name_users__user_id__name_patch",
268
  {"user_id": 1, "name": "XYZ"},
269
  )
270
 
@@ -282,9 +280,7 @@ class TestTools:
282
 
283
  # Check that the user was updated via MCP
284
  async with Client(fastmcp_openapi_server) as client:
285
- user_response = await client.read_resource(
286
- "resource://get_user_users__user_id__get/1"
287
- )
288
  assert isinstance(user_response[0], TextResourceContents)
289
  response_text = user_response[0].text
290
  user = json.loads(response_text)
@@ -387,18 +383,14 @@ class TestResourceTemplates:
387
  async with Client(fastmcp_openapi_server) as client:
388
  resource_templates = await client.list_resource_templates()
389
  assert len(resource_templates) == 2
390
- assert resource_templates[0].name == "get_user_users__user_id__get"
391
  assert (
392
- resource_templates[0].uriTemplate
393
- == r"resource://get_user_users__user_id__get/{user_id}"
394
- )
395
- assert (
396
- resource_templates[1].name
397
- == "get_user_active_state_users__user_id___is_active__get"
398
  )
 
399
  assert (
400
  resource_templates[1].uriTemplate
401
- == r"resource://get_user_active_state_users__user_id___is_active__get/{is_active}/{user_id}"
402
  )
403
 
404
  async def test_get_resource_template(
@@ -413,7 +405,7 @@ class TestResourceTemplates:
413
  user_id = 2
414
  async with Client(fastmcp_openapi_server) as client:
415
  resource_response = await client.read_resource(
416
- f"resource://get_user_users__user_id__get/{user_id}"
417
  )
418
  assert isinstance(resource_response[0], TextResourceContents)
419
  response_text = resource_response[0].text
@@ -436,7 +428,7 @@ class TestResourceTemplates:
436
  is_active = True
437
  async with Client(fastmcp_openapi_server) as client:
438
  resource_response = await client.read_resource(
439
- f"resource://get_user_active_state_users__user_id___is_active__get/{is_active}/{user_id}"
440
  )
441
  assert isinstance(resource_response[0], TextResourceContents)
442
  response_text = resource_response[0].text
@@ -472,11 +464,7 @@ class TestTagTransfer:
472
  (t for t in tools if t.name == "create_user_users_post"), None
473
  )
474
  update_user_tool = next(
475
- (
476
- t
477
- for t in tools
478
- if t.name == "update_user_name_users__user_id__name_patch"
479
- ),
480
  None,
481
  )
482
 
@@ -524,7 +512,7 @@ class TestTagTransfer:
524
 
525
  # Find the get_user template
526
  get_user_template = next(
527
- (t for t in templates if t.name == "get_user_users__user_id__get"), None
528
  )
529
 
530
  assert get_user_template is not None
@@ -545,7 +533,7 @@ class TestTagTransfer:
545
 
546
  # Find the get_user template
547
  get_user_template = next(
548
- (t for t in templates if t.name == "get_user_users__user_id__get"), None
549
  )
550
 
551
  assert get_user_template is not None
@@ -553,7 +541,7 @@ class TestTagTransfer:
553
  # Manually create a resource from template
554
  params = {"user_id": 1}
555
  resource = await get_user_template.create_resource(
556
- "resource://get_user_users__user_id__get/1", params
557
  )
558
 
559
  # Verify tags are preserved from template to resource
@@ -997,7 +985,7 @@ async def test_none_path_parameters_rejected(
997
  # get_user has a required path parameter user_id
998
  with pytest.raises(ToolError, match="Missing required path parameters"):
999
  await client.call_tool(
1000
- "update_user_name_users__user_id__name_patch",
1001
  {
1002
  "user_id": None, # This should cause an error
1003
  "name": "New Name",
@@ -1560,9 +1548,7 @@ class TestFastAPIDescriptionPropagation:
1560
  async def test_template_includes_function_docstring(self, fastapi_server):
1561
  """Test that a ResourceTemplate includes the function docstring."""
1562
  templates = list(fastapi_server._resource_manager.get_templates().values())
1563
- get_template = next(
1564
- (t for t in templates if "items__item_id__get" in t.name), None
1565
- )
1566
 
1567
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1568
  description = get_template.description or ""
@@ -1577,9 +1563,7 @@ class TestFastAPIDescriptionPropagation:
1577
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1578
  """
1579
  templates = list(fastapi_server._resource_manager.get_templates().values())
1580
- get_template = next(
1581
- (t for t in templates if "items__item_id__get" in t.name), None
1582
- )
1583
 
1584
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1585
  description = get_template.description or ""
@@ -1599,9 +1583,7 @@ class TestFastAPIDescriptionPropagation:
1599
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1600
  """
1601
  templates = list(fastapi_server._resource_manager.get_templates().values())
1602
- get_template = next(
1603
- (t for t in templates if "items__item_id__get" in t.name), None
1604
- )
1605
 
1606
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1607
  description = get_template.description or ""
@@ -1617,9 +1599,7 @@ class TestFastAPIDescriptionPropagation:
1617
  async def test_template_parameter_schema_includes_description(self, fastapi_server):
1618
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1619
  templates = list(fastapi_server._resource_manager.get_templates().values())
1620
- get_template = next(
1621
- (t for t in templates if "items__item_id__get" in t.name), None
1622
- )
1623
 
1624
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1625
  assert "properties" in get_template.parameters, (
@@ -1691,7 +1671,7 @@ class TestFastAPIDescriptionPropagation:
1691
  async with Client(fastapi_server) as client:
1692
  templates = await client.list_resource_templates()
1693
  get_template = next(
1694
- (t for t in templates if "items__item_id__get" in t.name), None
1695
  )
1696
 
1697
  assert get_template is not None, (
@@ -1821,9 +1801,7 @@ class TestEnumHandling:
1821
  tools = server._tool_manager.list_tools()
1822
 
1823
  # Find the read_item tool
1824
- read_item_tool = next(
1825
- (t for t in tools if t.name == "read_item_items__item_id__post"), None
1826
- )
1827
 
1828
  # Verify the tool exists
1829
  assert read_item_tool is not None, "read_item tool wasn't created"
@@ -2136,3 +2114,287 @@ class TestRouteMapTags:
2136
  "getMetrics",
2137
  }
2138
  assert tool_names == expected_tools
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  },
209
  )
210
  assert tools[1].model_dump() == dict(
211
+ name="update_user_name_users",
212
  annotations=None,
213
  description=IsStr(
214
  regex=r"^Update a user's name\..*$", regex_flags=re.DOTALL
 
248
 
249
  # Check that the user was created via MCP
250
  async with Client(fastmcp_openapi_server) as client:
251
+ user_response = await client.read_resource("resource://get_user_users/4")
 
 
252
  assert isinstance(user_response[0], TextResourceContents)
253
  response_text = user_response[0].text
254
  user = json.loads(response_text)
 
262
  """
263
  async with Client(fastmcp_openapi_server) as client:
264
  tool_response = await client.call_tool(
265
+ "update_user_name_users",
266
  {"user_id": 1, "name": "XYZ"},
267
  )
268
 
 
280
 
281
  # Check that the user was updated via MCP
282
  async with Client(fastmcp_openapi_server) as client:
283
+ user_response = await client.read_resource("resource://get_user_users/1")
 
 
284
  assert isinstance(user_response[0], TextResourceContents)
285
  response_text = user_response[0].text
286
  user = json.loads(response_text)
 
383
  async with Client(fastmcp_openapi_server) as client:
384
  resource_templates = await client.list_resource_templates()
385
  assert len(resource_templates) == 2
386
+ assert resource_templates[0].name == "get_user_users"
387
  assert (
388
+ resource_templates[0].uriTemplate == r"resource://get_user_users/{user_id}"
 
 
 
 
 
389
  )
390
+ assert resource_templates[1].name == "get_user_active_state_users"
391
  assert (
392
  resource_templates[1].uriTemplate
393
+ == r"resource://get_user_active_state_users/{is_active}/{user_id}"
394
  )
395
 
396
  async def test_get_resource_template(
 
405
  user_id = 2
406
  async with Client(fastmcp_openapi_server) as client:
407
  resource_response = await client.read_resource(
408
+ f"resource://get_user_users/{user_id}"
409
  )
410
  assert isinstance(resource_response[0], TextResourceContents)
411
  response_text = resource_response[0].text
 
428
  is_active = True
429
  async with Client(fastmcp_openapi_server) as client:
430
  resource_response = await client.read_resource(
431
+ f"resource://get_user_active_state_users/{is_active}/{user_id}"
432
  )
433
  assert isinstance(resource_response[0], TextResourceContents)
434
  response_text = resource_response[0].text
 
464
  (t for t in tools if t.name == "create_user_users_post"), None
465
  )
466
  update_user_tool = next(
467
+ (t for t in tools if t.name == "update_user_name_users"),
 
 
 
 
468
  None,
469
  )
470
 
 
512
 
513
  # Find the get_user template
514
  get_user_template = next(
515
+ (t for t in templates if t.name == "get_user_users"), None
516
  )
517
 
518
  assert get_user_template is not None
 
533
 
534
  # Find the get_user template
535
  get_user_template = next(
536
+ (t for t in templates if t.name == "get_user_users"), None
537
  )
538
 
539
  assert get_user_template is not None
 
541
  # Manually create a resource from template
542
  params = {"user_id": 1}
543
  resource = await get_user_template.create_resource(
544
+ "resource://get_user_users/1", params
545
  )
546
 
547
  # Verify tags are preserved from template to resource
 
985
  # get_user has a required path parameter user_id
986
  with pytest.raises(ToolError, match="Missing required path parameters"):
987
  await client.call_tool(
988
+ "update_user_name_users",
989
  {
990
  "user_id": None, # This should cause an error
991
  "name": "New Name",
 
1548
  async def test_template_includes_function_docstring(self, fastapi_server):
1549
  """Test that a ResourceTemplate includes the function docstring."""
1550
  templates = list(fastapi_server._resource_manager.get_templates().values())
1551
+ get_template = next((t for t in templates if "get_item_items" in t.name), None)
 
 
1552
 
1553
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1554
  description = get_template.description or ""
 
1563
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1564
  """
1565
  templates = list(fastapi_server._resource_manager.get_templates().values())
1566
+ get_template = next((t for t in templates if "get_item_items" in t.name), None)
 
 
1567
 
1568
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1569
  description = get_template.description or ""
 
1583
  are not properly propagated to the OpenAPI schema. The parameters appear but without the description.
1584
  """
1585
  templates = list(fastapi_server._resource_manager.get_templates().values())
1586
+ get_template = next((t for t in templates if "get_item_items" in t.name), None)
 
 
1587
 
1588
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1589
  description = get_template.description or ""
 
1599
  async def test_template_parameter_schema_includes_description(self, fastapi_server):
1600
  """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1601
  templates = list(fastapi_server._resource_manager.get_templates().values())
1602
+ get_template = next((t for t in templates if "get_item_items" in t.name), None)
 
 
1603
 
1604
  assert get_template is not None, "GET /items/{item_id} template wasn't created"
1605
  assert "properties" in get_template.parameters, (
 
1671
  async with Client(fastapi_server) as client:
1672
  templates = await client.list_resource_templates()
1673
  get_template = next(
1674
+ (t for t in templates if "get_item_items" in t.name), None
1675
  )
1676
 
1677
  assert get_template is not None, (
 
1801
  tools = server._tool_manager.list_tools()
1802
 
1803
  # Find the read_item tool
1804
+ read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
 
 
1805
 
1806
  # Verify the tool exists
1807
  assert read_item_tool is not None, "read_item tool wasn't created"
 
2114
  "getMetrics",
2115
  }
2116
  assert tool_names == expected_tools
2117
+
2118
+
2119
+ class TestMCPNames:
2120
+ """Tests for the mcp_names dictionary functionality."""
2121
+
2122
+ @pytest.fixture
2123
+ def mcp_names_openapi_spec(self) -> dict:
2124
+ """OpenAPI spec with various operationIds for testing naming strategies."""
2125
+ return {
2126
+ "openapi": "3.1.0",
2127
+ "info": {"title": "MCP Names Test API", "version": "1.0.0"},
2128
+ "paths": {
2129
+ "/users": {
2130
+ "get": {
2131
+ "operationId": "list_users__with_pagination",
2132
+ "summary": "Get All Users",
2133
+ "responses": {"200": {"description": "Success"}},
2134
+ },
2135
+ "post": {
2136
+ "operationId": "create_user_admin__special_permissions",
2137
+ "summary": "Create New User",
2138
+ "requestBody": {
2139
+ "required": True,
2140
+ "content": {
2141
+ "application/json": {
2142
+ "schema": {
2143
+ "type": "object",
2144
+ "properties": {"name": {"type": "string"}},
2145
+ "required": ["name"],
2146
+ }
2147
+ }
2148
+ },
2149
+ },
2150
+ "responses": {"201": {"description": "Created"}},
2151
+ },
2152
+ },
2153
+ "/users/{id}": {
2154
+ "get": {
2155
+ "operationId": "get_user_by_id__admin_only",
2156
+ "summary": "Fetch Single User Profile",
2157
+ "parameters": [
2158
+ {
2159
+ "name": "id",
2160
+ "in": "path",
2161
+ "required": True,
2162
+ "schema": {"type": "integer"},
2163
+ }
2164
+ ],
2165
+ "responses": {"200": {"description": "Success"}},
2166
+ }
2167
+ },
2168
+ "/very-long-endpoint-name": {
2169
+ "get": {
2170
+ "operationId": "this_is_a_very_long_operation_id_that_exceeds_fifty_six_characters_and_should_be_truncated",
2171
+ "summary": "This Is A Very Long Summary That Should Also Be Truncated When Used As Name",
2172
+ "responses": {"200": {"description": "Success"}},
2173
+ }
2174
+ },
2175
+ "/special": {
2176
+ "get": {
2177
+ "operationId": "special-chars@and#spaces in$operation%id",
2178
+ "summary": "Special Chars & Spaces In Summary!",
2179
+ "responses": {"200": {"description": "Success"}},
2180
+ }
2181
+ },
2182
+ },
2183
+ }
2184
+
2185
+ @pytest.fixture
2186
+ async def mock_client(self) -> httpx.AsyncClient:
2187
+ """Mock client for testing."""
2188
+
2189
+ async def _responder(request):
2190
+ return httpx.Response(200, json={"status": "ok"})
2191
+
2192
+ transport = httpx.MockTransport(_responder)
2193
+ return httpx.AsyncClient(transport=transport, base_url="http://test")
2194
+
2195
+ async def test_mcp_names_custom_mapping(self, mcp_names_openapi_spec, mock_client):
2196
+ """Test that mcp_names dictionary provides custom names for components."""
2197
+ mcp_names = {
2198
+ "list_users__with_pagination": "user_list",
2199
+ "create_user_admin__special_permissions": "admin_create_user",
2200
+ "get_user_by_id__admin_only": "user_detail",
2201
+ }
2202
+
2203
+ server = FastMCPOpenAPI(
2204
+ openapi_spec=mcp_names_openapi_spec,
2205
+ client=mock_client,
2206
+ mcp_names=mcp_names,
2207
+ )
2208
+
2209
+ # Check tools use custom names
2210
+ tools = server._tool_manager.list_tools()
2211
+ tool_names = {tool.name for tool in tools}
2212
+ assert "admin_create_user" in tool_names
2213
+
2214
+ # Check resource templates use custom names
2215
+ templates = list(server._resource_manager.get_templates().values())
2216
+ template_names = {template.name for template in templates}
2217
+ assert "user_detail" in template_names
2218
+
2219
+ # Check resources use custom names
2220
+ resources = list(server._resource_manager.get_resources().values())
2221
+ resource_names = {resource.name for resource in resources}
2222
+ assert "user_list" in resource_names
2223
+
2224
+ async def test_mcp_names_fallback_to_operation_id_short(
2225
+ self, mcp_names_openapi_spec, mock_client
2226
+ ):
2227
+ """Test fallback to operationId up to double underscore when not in mcp_names."""
2228
+ # Only provide mapping for one operationId
2229
+ mcp_names = {
2230
+ "list_users__with_pagination": "custom_user_list",
2231
+ }
2232
+
2233
+ server = FastMCPOpenAPI(
2234
+ openapi_spec=mcp_names_openapi_spec,
2235
+ client=mock_client,
2236
+ mcp_names=mcp_names,
2237
+ )
2238
+
2239
+ tools = server._tool_manager.list_tools()
2240
+ tool_names = {tool.name for tool in tools}
2241
+
2242
+ templates = list(server._resource_manager.get_templates().values())
2243
+ template_names = {template.name for template in templates}
2244
+
2245
+ resources = list(server._resource_manager.get_resources().values())
2246
+ resource_names = {resource.name for resource in resources}
2247
+
2248
+ # Custom mapped name should be used
2249
+ assert "custom_user_list" in resource_names
2250
+
2251
+ # Unmapped operationIds should use short version (up to __)
2252
+ assert "create_user_admin" in tool_names
2253
+ assert "get_user_by_id" in template_names
2254
+
2255
+ async def test_names_are_slugified(self, mcp_names_openapi_spec, mock_client):
2256
+ """Test that names are properly slugified (spaces, special chars removed)."""
2257
+ server = FastMCPOpenAPI(
2258
+ openapi_spec=mcp_names_openapi_spec,
2259
+ client=mock_client,
2260
+ )
2261
+
2262
+ resources = list(server._resource_manager.get_resources().values())
2263
+ resource_names = {
2264
+ resource.name for resource in resources if resource.name is not None
2265
+ }
2266
+
2267
+ # Special chars and spaces should be slugified
2268
+ slugified_name = next(
2269
+ (name for name in resource_names if "special" in name), None
2270
+ )
2271
+ assert slugified_name is not None
2272
+ # Should not contain special characters or spaces
2273
+ assert "@" not in slugified_name
2274
+ assert "#" not in slugified_name
2275
+ assert "$" not in slugified_name
2276
+ assert "%" not in slugified_name
2277
+ assert " " not in slugified_name
2278
+
2279
+ async def test_names_are_truncated_to_56_chars(
2280
+ self, mcp_names_openapi_spec, mock_client
2281
+ ):
2282
+ """Test that names are truncated to 56 characters maximum."""
2283
+ server = FastMCPOpenAPI(
2284
+ openapi_spec=mcp_names_openapi_spec,
2285
+ client=mock_client,
2286
+ )
2287
+
2288
+ # Check all component types
2289
+ all_names = []
2290
+
2291
+ tools = server._tool_manager.list_tools()
2292
+ all_names.extend(tool.name for tool in tools)
2293
+
2294
+ resources = list(server._resource_manager.get_resources().values())
2295
+ all_names.extend(resource.name for resource in resources)
2296
+
2297
+ templates = list(server._resource_manager.get_templates().values())
2298
+ all_names.extend(template.name for template in templates)
2299
+
2300
+ # All names should be 56 characters or less
2301
+ for name in all_names:
2302
+ assert len(name) <= 56, (
2303
+ f"Name '{name}' exceeds 56 characters (length: {len(name)})"
2304
+ )
2305
+
2306
+ # Verify that the long operationId was actually truncated
2307
+ long_name = next((name for name in all_names if len(name) > 50), None)
2308
+ assert long_name is not None, "Expected to find a truncated name for testing"
2309
+
2310
+ async def test_mcp_names_with_from_openapi_classmethod(
2311
+ self, mcp_names_openapi_spec, mock_client
2312
+ ):
2313
+ """Test mcp_names works with FastMCP.from_openapi() classmethod."""
2314
+ mcp_names = {
2315
+ "list_users__with_pagination": "openapi_user_list",
2316
+ }
2317
+
2318
+ server = FastMCP.from_openapi(
2319
+ openapi_spec=mcp_names_openapi_spec,
2320
+ client=mock_client,
2321
+ mcp_names=mcp_names,
2322
+ )
2323
+
2324
+ resources = list(server._resource_manager.get_resources().values())
2325
+ resource_names = {resource.name for resource in resources}
2326
+ assert "openapi_user_list" in resource_names
2327
+
2328
+ async def test_mcp_names_with_from_fastapi_classmethod(self):
2329
+ """Test mcp_names works with FastMCP.from_fastapi() classmethod."""
2330
+ from fastapi import FastAPI
2331
+ from pydantic import BaseModel
2332
+
2333
+ app = FastAPI(title="FastAPI MCP Names Test")
2334
+
2335
+ class User(BaseModel):
2336
+ name: str
2337
+
2338
+ @app.get("/users", operation_id="list_users__with_filters")
2339
+ async def get_users() -> list[User]:
2340
+ return [User(name="test")]
2341
+
2342
+ @app.post("/users", operation_id="create_user__admin_required")
2343
+ async def create_user(user: User) -> User:
2344
+ return user
2345
+
2346
+ mcp_names = {
2347
+ "list_users__with_filters": "fastapi_user_list",
2348
+ "create_user__admin_required": "fastapi_create_user",
2349
+ }
2350
+
2351
+ server = FastMCP.from_fastapi(
2352
+ app=app,
2353
+ mcp_names=mcp_names,
2354
+ )
2355
+
2356
+ tools = server._tool_manager.list_tools()
2357
+ tool_names = {tool.name for tool in tools}
2358
+
2359
+ resources = list(server._resource_manager.get_resources().values())
2360
+ resource_names = {resource.name for resource in resources}
2361
+
2362
+ assert "fastapi_create_user" in tool_names
2363
+ assert "fastapi_user_list" in resource_names
2364
+
2365
+ async def test_mcp_names_custom_names_are_also_truncated(
2366
+ self, mcp_names_openapi_spec, mock_client
2367
+ ):
2368
+ """Test that custom names in mcp_names are also truncated to 56 characters."""
2369
+ # Provide a custom name that's longer than 56 characters
2370
+ very_long_custom_name = "this_is_a_very_long_custom_name_that_exceeds_fifty_six_characters_and_should_be_truncated"
2371
+
2372
+ mcp_names = {
2373
+ "list_users__with_pagination": very_long_custom_name,
2374
+ }
2375
+
2376
+ server = FastMCPOpenAPI(
2377
+ openapi_spec=mcp_names_openapi_spec,
2378
+ client=mock_client,
2379
+ mcp_names=mcp_names,
2380
+ )
2381
+
2382
+ resources = list(server._resource_manager.get_resources().values())
2383
+ resource_names = {
2384
+ resource.name for resource in resources if resource.name is not None
2385
+ }
2386
+
2387
+ # Find the resource that should have the custom name
2388
+ truncated_name = next(
2389
+ (
2390
+ name
2391
+ for name in resource_names
2392
+ if "this_is_a_very_long_custom_name" in name
2393
+ ),
2394
+ None,
2395
+ )
2396
+ assert truncated_name is not None
2397
+ assert len(truncated_name) <= 56
2398
+ assert (
2399
+ len(truncated_name) == 56
2400
+ ) # Should be exactly 56 since original was longer
tests/server/openapi/test_openapi_path_parameters.py CHANGED
@@ -84,7 +84,7 @@ async def test_fastmcp_from_openapi(array_path_spec, mock_client):
84
  # Verify the tool was created using the MCP protocol method
85
  tools_result = await mcp.get_tools()
86
  tool_names = [tool.name for tool in tools_result.values()]
87
- assert "test-operation" in tool_names
88
 
89
 
90
  async def test_array_path_parameter_handling(mock_client):
@@ -93,7 +93,7 @@ async def test_array_path_parameter_handling(mock_client):
93
  route = HTTPRoute(
94
  path="/select/{days}",
95
  method="PUT",
96
- operation_id="test-operation",
97
  parameters=[
98
  ParameterInfo(
99
  name="days",
@@ -122,7 +122,7 @@ async def test_array_path_parameter_handling(mock_client):
122
  tool = OpenAPITool(
123
  client=mock_client,
124
  route=route,
125
- name="test-operation",
126
  description="Test operation",
127
  parameters={},
128
  )
@@ -163,7 +163,7 @@ async def test_integration_array_path_parameter(array_path_spec, mock_client):
163
  mcp = FastMCP.from_openapi(array_path_spec, client=mock_client)
164
 
165
  # Call the tool with a single value
166
- await mcp._mcp_call_tool("test-operation", {"days": ["monday"]})
167
 
168
  # Check the request was made correctly
169
  mock_client.request.assert_called_with(
@@ -177,7 +177,7 @@ async def test_integration_array_path_parameter(array_path_spec, mock_client):
177
  mock_client.request.reset_mock()
178
 
179
  # Call the tool with multiple values
180
- await mcp._mcp_call_tool("test-operation", {"days": ["monday", "tuesday"]})
181
 
182
  # Check the request was made correctly
183
  mock_client.request.assert_called_with(
 
84
  # Verify the tool was created using the MCP protocol method
85
  tools_result = await mcp.get_tools()
86
  tool_names = [tool.name for tool in tools_result.values()]
87
+ assert "test_operation" in tool_names
88
 
89
 
90
  async def test_array_path_parameter_handling(mock_client):
 
93
  route = HTTPRoute(
94
  path="/select/{days}",
95
  method="PUT",
96
+ operation_id="test_operation",
97
  parameters=[
98
  ParameterInfo(
99
  name="days",
 
122
  tool = OpenAPITool(
123
  client=mock_client,
124
  route=route,
125
+ name="test_operation",
126
  description="Test operation",
127
  parameters={},
128
  )
 
163
  mcp = FastMCP.from_openapi(array_path_spec, client=mock_client)
164
 
165
  # Call the tool with a single value
166
+ await mcp._mcp_call_tool("test_operation", {"days": ["monday"]})
167
 
168
  # Check the request was made correctly
169
  mock_client.request.assert_called_with(
 
177
  mock_client.request.reset_mock()
178
 
179
  # Call the tool with multiple values
180
+ await mcp._mcp_call_tool("test_operation", {"days": ["monday", "tuesday"]})
181
 
182
  # Check the request was made correctly
183
  mock_client.request.assert_called_with(