Jeremiah Lowin commited on
Commit
451db43
·
1 Parent(s): 88b4a4a

Update name generation

Browse files
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,26 @@ 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 +716,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 +734,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 +748,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 +799,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 +836,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 +855,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 +901,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 +946,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
+ """Convert text to a URL-friendly slug format."""
42
+ if not text:
43
+ return ""
44
+
45
+ # Replace spaces and common separators with underscores
46
+ slug = re.sub(r"[\s\-\.]+", "_", text)
47
+
48
+ # Remove non-alphanumeric characters except underscores
49
+ slug = re.sub(r"[^a-zA-Z0-9_]", "", slug)
50
+
51
+ # Remove multiple consecutive underscores
52
+ slug = re.sub(r"_+", "_", slug)
53
+
54
+ # Remove leading/trailing underscores
55
+ slug = slug.strip("_")
56
+
57
+ return slug
58
+
59
+
60
  def _get_mcp_client_headers() -> dict[str, str]:
61
  """
62
  Extract headers from the current MCP client HTTP request if available.
 
716
  route_maps: list[RouteMap] | None = None,
717
  route_map_fn: RouteMapFn | None = None,
718
  mcp_component_fn: ComponentFn | None = None,
719
+ mcp_names: dict[str, str] | None = None,
720
  timeout: float | None = None,
721
  **settings: Any,
722
  ):
 
734
  mcp_component_fn: Optional callable for component customization.
735
  Receives (route, component) and can modify the component in-place.
736
  Called on every created component.
737
+ mcp_names: Optional dictionary mapping operationId to desired component names.
738
+ If an operationId is not in the dictionary, falls back to using the
739
+ operationId up to the first double underscore. If no operationId exists,
740
+ falls back to slugified summary or path-based naming.
741
+ All names are truncated to 56 characters maximum.
742
  timeout: Optional timeout (in seconds) for all requests
743
  **settings: Additional settings for FastMCP
744
  """
 
748
  self._timeout = timeout
749
  self._route_map_fn = route_map_fn
750
  self._mcp_component_fn = mcp_component_fn
751
+ self._mcp_names = mcp_names or {}
752
 
753
  # Keep track of names to detect collisions
754
+ self._used_names = {
755
+ "tool": Counter(),
756
+ "resource": Counter(),
757
+ "resource_template": Counter(),
758
+ "prompt": Counter(),
759
+ }
760
 
761
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
762
 
 
799
  def _generate_default_name(
800
  self, route: openapi.HTTPRoute, mcp_type: MCPType
801
  ) -> str:
802
+ """Generate a default name from the route using the configured strategy."""
803
+ name = ""
804
 
805
+ # First check if there's a custom mapping for this operationId
806
  if route.operation_id:
807
+ if route.operation_id in self._mcp_names:
808
+ name = self._mcp_names[route.operation_id]
 
 
 
 
 
 
 
 
 
 
 
809
  else:
810
+ # If there's a double underscore, use the first part
811
+ name = route.operation_id.split("__")[0]
812
+ else:
813
+ name = route.summary or f"{route.method}_{route.path}"
814
 
815
+ name = _slugify(name)
 
816
 
817
+ # Truncate to 56 characters maximum
818
+ if len(name) > 56:
819
+ name = name[:56]
 
 
 
820
 
821
+ return name
822
 
823
  def _get_unique_name(
824
+ self,
825
+ name: str,
826
+ component_type: Literal["tool", "resource", "resource_template", "prompt"],
827
  ) -> str:
828
  """
829
  Ensure the name is unique within its component type by appending numbers if needed.
 
836
  str: A unique name for the component
837
  """
838
  # Check if the name is already used
839
+ self._used_names[component_type][name] += 1
840
+ if self._used_names[component_type][name] == 1:
841
  return name
842
 
843
+ else:
844
+ # Create the new name
845
+ new_name = f"{name}_{self._used_names[component_type][name]}"
846
+ logger.debug(
847
+ f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
848
+ f"Using '{new_name}' instead."
849
+ )
 
 
 
 
850
 
 
851
  return new_name
852
 
853
  def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
 
855
  combined_schema = _combine_schemas(route)
856
 
857
  # Get a unique tool name
858
+ tool_name = self._get_unique_name(name, "tool")
859
 
860
  base_description = (
861
  route.description
 
901
  def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
902
  """Creates and registers an OpenAPIResource with enhanced description."""
903
  # Get a unique resource name
904
+ resource_name = self._get_unique_name(name, "resource")
905
 
906
  resource_uri = f"resource://{resource_name}"
907
  base_description = (
 
946
  def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
947
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
948
  # Get a unique template name
949
+ template_name = self._get_unique_name(name, "resource_template")
950
 
951
  path_params = [p.name for p in route.parameters if p.location == "path"]
952
  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