Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
451db43
1
Parent(s): 88b4a4a
Update name generation
Browse files- src/fastmcp/server/openapi.py +63 -44
- src/fastmcp/server/server.py +4 -0
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 = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 770 |
-
|
| 771 |
|
|
|
|
| 772 |
if route.operation_id:
|
| 773 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 788 |
|
| 789 |
-
|
| 790 |
-
resource_name = "_".join(clean_parts)
|
| 791 |
|
| 792 |
-
#
|
| 793 |
-
if
|
| 794 |
-
|
| 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
|
| 800 |
|
| 801 |
def _get_unique_name(
|
| 802 |
-
self,
|
|
|
|
|
|
|
| 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 |
-
|
| 816 |
-
|
| 817 |
return name
|
| 818 |
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 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, "
|
| 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, "
|
| 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, "
|
| 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 |
|