Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
00da27f
1
Parent(s): 23dba76
Clean up route maps
Browse files
src/fastmcp/server/openapi.py
CHANGED
|
@@ -103,15 +103,27 @@ class RouteType(enum.Enum):
|
|
| 103 |
IGNORE = "IGNORE"
|
| 104 |
|
| 105 |
|
| 106 |
-
@dataclass
|
| 107 |
class RouteMap:
|
| 108 |
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
| 109 |
|
| 110 |
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
| 111 |
pattern: Pattern[str] | str = field(default=r".*")
|
| 112 |
-
mcp_type: MCPType | None = field(default=None)
|
| 113 |
route_type: RouteType | MCPType | None = field(default=None)
|
| 114 |
-
tags: set[str] = field(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
def __post_init__(self):
|
| 117 |
"""Validate and process the route map after initialization."""
|
|
@@ -165,7 +177,7 @@ DEFAULT_ROUTE_MAPPINGS = [
|
|
| 165 |
def _determine_route_type(
|
| 166 |
route: openapi.HTTPRoute,
|
| 167 |
mappings: list[RouteMap],
|
| 168 |
-
) ->
|
| 169 |
"""
|
| 170 |
Determines the FastMCP component type based on the route and mappings.
|
| 171 |
|
|
@@ -174,7 +186,7 @@ def _determine_route_type(
|
|
| 174 |
mappings: List of RouteMap objects in priority order
|
| 175 |
|
| 176 |
Returns:
|
| 177 |
-
|
| 178 |
"""
|
| 179 |
# Check mappings in priority order (first match wins)
|
| 180 |
for route_map in mappings:
|
|
@@ -201,10 +213,10 @@ def _determine_route_type(
|
|
| 201 |
logger.debug(
|
| 202 |
f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
|
| 203 |
)
|
| 204 |
-
return route_map
|
| 205 |
|
| 206 |
# Default fallback
|
| 207 |
-
return MCPType.TOOL
|
| 208 |
|
| 209 |
|
| 210 |
class OpenAPITool(Tool):
|
|
@@ -217,7 +229,7 @@ class OpenAPITool(Tool):
|
|
| 217 |
name: str,
|
| 218 |
description: str,
|
| 219 |
parameters: dict[str, Any],
|
| 220 |
-
tags: set[str] =
|
| 221 |
timeout: float | None = None,
|
| 222 |
annotations: ToolAnnotations | None = None,
|
| 223 |
serializer: Callable[[Any], str] | None = None,
|
|
@@ -226,7 +238,7 @@ class OpenAPITool(Tool):
|
|
| 226 |
name=name,
|
| 227 |
description=description,
|
| 228 |
parameters=parameters,
|
| 229 |
-
tags=tags,
|
| 230 |
annotations=annotations,
|
| 231 |
serializer=serializer,
|
| 232 |
)
|
|
@@ -680,6 +692,8 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 680 |
route_map_fn: RouteMapFn | None = None,
|
| 681 |
mcp_component_fn: ComponentFn | None = None,
|
| 682 |
mcp_names: dict[str, str] | None = None,
|
|
|
|
|
|
|
| 683 |
timeout: float | None = None,
|
| 684 |
**settings: Any,
|
| 685 |
):
|
|
@@ -702,6 +716,11 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 702 |
operationId up to the first double underscore. If no operationId exists,
|
| 703 |
falls back to slugified summary or path-based naming.
|
| 704 |
All names are truncated to 56 characters maximum.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
timeout: Optional timeout (in seconds) for all requests
|
| 706 |
**settings: Additional settings for FastMCP
|
| 707 |
"""
|
|
@@ -709,9 +728,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 709 |
|
| 710 |
self._client = client
|
| 711 |
self._timeout = timeout
|
| 712 |
-
self._route_map_fn = route_map_fn
|
| 713 |
self._mcp_component_fn = mcp_component_fn
|
| 714 |
-
self._mcp_names = mcp_names or {}
|
| 715 |
|
| 716 |
# Keep track of names to detect collisions
|
| 717 |
self._used_names = {
|
|
@@ -727,12 +744,16 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 727 |
route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
|
| 728 |
for route in http_routes:
|
| 729 |
# Determine route type based on mappings or default rules
|
| 730 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
|
| 732 |
# Call route_map_fn if provided
|
| 733 |
-
if
|
| 734 |
try:
|
| 735 |
-
result =
|
| 736 |
if result is not None:
|
| 737 |
route_type = result
|
| 738 |
logger.debug(
|
|
@@ -746,29 +767,34 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 746 |
)
|
| 747 |
|
| 748 |
# Generate a default name from the route
|
| 749 |
-
component_name = self._generate_default_name(route,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
|
| 751 |
if route_type == MCPType.TOOL:
|
| 752 |
-
self._create_openapi_tool(route, component_name)
|
| 753 |
elif route_type == MCPType.RESOURCE:
|
| 754 |
-
self._create_openapi_resource(route, component_name)
|
| 755 |
elif route_type == MCPType.RESOURCE_TEMPLATE:
|
| 756 |
-
self._create_openapi_template(route, component_name)
|
| 757 |
elif route_type == MCPType.EXCLUDE:
|
| 758 |
logger.info(f"Excluding route: {route.method} {route.path}")
|
| 759 |
|
| 760 |
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
| 761 |
|
| 762 |
def _generate_default_name(
|
| 763 |
-
self, route: openapi.HTTPRoute,
|
| 764 |
) -> str:
|
| 765 |
"""Generate a default name from the route using the configured strategy."""
|
| 766 |
name = ""
|
|
|
|
| 767 |
|
| 768 |
# First check if there's a custom mapping for this operationId
|
| 769 |
if route.operation_id:
|
| 770 |
-
if route.operation_id in
|
| 771 |
-
name =
|
| 772 |
else:
|
| 773 |
# If there's a double underscore in the operationId, use the first part
|
| 774 |
name = route.operation_id.split("__")[0]
|
|
@@ -813,7 +839,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 813 |
|
| 814 |
return new_name
|
| 815 |
|
| 816 |
-
def _create_openapi_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
"""Creates and registers an OpenAPITool with enhanced description."""
|
| 818 |
combined_schema = _combine_schemas(route)
|
| 819 |
|
|
@@ -840,7 +871,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 840 |
name=tool_name,
|
| 841 |
description=enhanced_description,
|
| 842 |
parameters=combined_schema,
|
| 843 |
-
tags=set(route.tags or []),
|
| 844 |
timeout=self._timeout,
|
| 845 |
)
|
| 846 |
|
|
@@ -861,7 +892,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 861 |
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
| 862 |
)
|
| 863 |
|
| 864 |
-
def _create_openapi_resource(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 865 |
"""Creates and registers an OpenAPIResource with enhanced description."""
|
| 866 |
# Get a unique resource name
|
| 867 |
resource_name = self._get_unique_name(name, "resource")
|
|
@@ -885,7 +921,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 885 |
uri=resource_uri,
|
| 886 |
name=resource_name,
|
| 887 |
description=enhanced_description,
|
| 888 |
-
tags=set(route.tags or []),
|
| 889 |
timeout=self._timeout,
|
| 890 |
)
|
| 891 |
|
|
@@ -906,7 +942,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 906 |
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
| 907 |
)
|
| 908 |
|
| 909 |
-
def _create_openapi_template(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
"""Creates and registers an OpenAPIResourceTemplate with enhanced description."""
|
| 911 |
# Get a unique template name
|
| 912 |
template_name = self._get_unique_name(name, "resource_template")
|
|
@@ -959,7 +1000,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 959 |
name=template_name,
|
| 960 |
description=enhanced_description,
|
| 961 |
parameters=template_params_schema,
|
| 962 |
-
tags=set(route.tags or []),
|
| 963 |
timeout=self._timeout,
|
| 964 |
)
|
| 965 |
|
|
|
|
| 103 |
IGNORE = "IGNORE"
|
| 104 |
|
| 105 |
|
| 106 |
+
@dataclass(kw_only=True)
|
| 107 |
class RouteMap:
|
| 108 |
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
| 109 |
|
| 110 |
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
| 111 |
pattern: Pattern[str] | str = field(default=r".*")
|
|
|
|
| 112 |
route_type: RouteType | MCPType | None = field(default=None)
|
| 113 |
+
tags: set[str] = field(
|
| 114 |
+
default_factory=set,
|
| 115 |
+
metadata={"description": "A set of tags to match. All tags must match."},
|
| 116 |
+
)
|
| 117 |
+
mcp_type: MCPType | None = field(
|
| 118 |
+
default=None,
|
| 119 |
+
metadata={"description": "The type of FastMCP component to create."},
|
| 120 |
+
)
|
| 121 |
+
mcp_tags: set[str] = field(
|
| 122 |
+
default_factory=set,
|
| 123 |
+
metadata={
|
| 124 |
+
"description": "A set of tags to apply to the generated FastMCP component."
|
| 125 |
+
},
|
| 126 |
+
)
|
| 127 |
|
| 128 |
def __post_init__(self):
|
| 129 |
"""Validate and process the route map after initialization."""
|
|
|
|
| 177 |
def _determine_route_type(
|
| 178 |
route: openapi.HTTPRoute,
|
| 179 |
mappings: list[RouteMap],
|
| 180 |
+
) -> RouteMap:
|
| 181 |
"""
|
| 182 |
Determines the FastMCP component type based on the route and mappings.
|
| 183 |
|
|
|
|
| 186 |
mappings: List of RouteMap objects in priority order
|
| 187 |
|
| 188 |
Returns:
|
| 189 |
+
The RouteMap that matches the route, or a catchall "Tool" RouteMap if no match is found.
|
| 190 |
"""
|
| 191 |
# Check mappings in priority order (first match wins)
|
| 192 |
for route_map in mappings:
|
|
|
|
| 213 |
logger.debug(
|
| 214 |
f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
|
| 215 |
)
|
| 216 |
+
return route_map
|
| 217 |
|
| 218 |
# Default fallback
|
| 219 |
+
return RouteMap(mcp_type=MCPType.TOOL)
|
| 220 |
|
| 221 |
|
| 222 |
class OpenAPITool(Tool):
|
|
|
|
| 229 |
name: str,
|
| 230 |
description: str,
|
| 231 |
parameters: dict[str, Any],
|
| 232 |
+
tags: set[str] | None = None,
|
| 233 |
timeout: float | None = None,
|
| 234 |
annotations: ToolAnnotations | None = None,
|
| 235 |
serializer: Callable[[Any], str] | None = None,
|
|
|
|
| 238 |
name=name,
|
| 239 |
description=description,
|
| 240 |
parameters=parameters,
|
| 241 |
+
tags=tags or set(),
|
| 242 |
annotations=annotations,
|
| 243 |
serializer=serializer,
|
| 244 |
)
|
|
|
|
| 692 |
route_map_fn: RouteMapFn | None = None,
|
| 693 |
mcp_component_fn: ComponentFn | None = None,
|
| 694 |
mcp_names: dict[str, str] | None = None,
|
| 695 |
+
mcp_tags: dict[str, set[str]] | None = None,
|
| 696 |
+
tags: set[str] | None = None,
|
| 697 |
timeout: float | None = None,
|
| 698 |
**settings: Any,
|
| 699 |
):
|
|
|
|
| 716 |
operationId up to the first double underscore. If no operationId exists,
|
| 717 |
falls back to slugified summary or path-based naming.
|
| 718 |
All names are truncated to 56 characters maximum.
|
| 719 |
+
mcp_tags: Optional dictionary mapping operationId to set of tags.
|
| 720 |
+
If an operationId is not in the dictionary, falls back to using the
|
| 721 |
+
tags from the route.
|
| 722 |
+
tags: Optional set of tags to add to all components. Components always receive any tags
|
| 723 |
+
from the route.
|
| 724 |
timeout: Optional timeout (in seconds) for all requests
|
| 725 |
**settings: Additional settings for FastMCP
|
| 726 |
"""
|
|
|
|
| 728 |
|
| 729 |
self._client = client
|
| 730 |
self._timeout = timeout
|
|
|
|
| 731 |
self._mcp_component_fn = mcp_component_fn
|
|
|
|
| 732 |
|
| 733 |
# Keep track of names to detect collisions
|
| 734 |
self._used_names = {
|
|
|
|
| 744 |
route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
|
| 745 |
for route in http_routes:
|
| 746 |
# Determine route type based on mappings or default rules
|
| 747 |
+
route_map = _determine_route_type(route, route_maps)
|
| 748 |
+
|
| 749 |
+
# TODO: remove this once RouteType is removed and mcp_type is typed as MCPType without | None
|
| 750 |
+
assert route_map.mcp_type is not None
|
| 751 |
+
route_type = route_map.mcp_type
|
| 752 |
|
| 753 |
# Call route_map_fn if provided
|
| 754 |
+
if route_map_fn is not None:
|
| 755 |
try:
|
| 756 |
+
result = route_map_fn(route, route_type)
|
| 757 |
if result is not None:
|
| 758 |
route_type = result
|
| 759 |
logger.debug(
|
|
|
|
| 767 |
)
|
| 768 |
|
| 769 |
# Generate a default name from the route
|
| 770 |
+
component_name = self._generate_default_name(route, mcp_names)
|
| 771 |
+
|
| 772 |
+
route_tags = set(route.tags) | route_map.mcp_tags | (tags or set())
|
| 773 |
+
if route.operation_id:
|
| 774 |
+
route_tags |= (mcp_tags or {}).get(route.operation_id, set())
|
| 775 |
|
| 776 |
if route_type == MCPType.TOOL:
|
| 777 |
+
self._create_openapi_tool(route, component_name, tags=route_tags)
|
| 778 |
elif route_type == MCPType.RESOURCE:
|
| 779 |
+
self._create_openapi_resource(route, component_name, tags=route_tags)
|
| 780 |
elif route_type == MCPType.RESOURCE_TEMPLATE:
|
| 781 |
+
self._create_openapi_template(route, component_name, tags=route_tags)
|
| 782 |
elif route_type == MCPType.EXCLUDE:
|
| 783 |
logger.info(f"Excluding route: {route.method} {route.path}")
|
| 784 |
|
| 785 |
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
| 786 |
|
| 787 |
def _generate_default_name(
|
| 788 |
+
self, route: openapi.HTTPRoute, mcp_names_map: dict[str, str] | None = None
|
| 789 |
) -> str:
|
| 790 |
"""Generate a default name from the route using the configured strategy."""
|
| 791 |
name = ""
|
| 792 |
+
mcp_names_map = mcp_names_map or {}
|
| 793 |
|
| 794 |
# First check if there's a custom mapping for this operationId
|
| 795 |
if route.operation_id:
|
| 796 |
+
if route.operation_id in mcp_names_map:
|
| 797 |
+
name = mcp_names_map[route.operation_id]
|
| 798 |
else:
|
| 799 |
# If there's a double underscore in the operationId, use the first part
|
| 800 |
name = route.operation_id.split("__")[0]
|
|
|
|
| 839 |
|
| 840 |
return new_name
|
| 841 |
|
| 842 |
+
def _create_openapi_tool(
|
| 843 |
+
self,
|
| 844 |
+
route: openapi.HTTPRoute,
|
| 845 |
+
name: str,
|
| 846 |
+
tags: set[str],
|
| 847 |
+
):
|
| 848 |
"""Creates and registers an OpenAPITool with enhanced description."""
|
| 849 |
combined_schema = _combine_schemas(route)
|
| 850 |
|
|
|
|
| 871 |
name=tool_name,
|
| 872 |
description=enhanced_description,
|
| 873 |
parameters=combined_schema,
|
| 874 |
+
tags=set(route.tags or []) | tags,
|
| 875 |
timeout=self._timeout,
|
| 876 |
)
|
| 877 |
|
|
|
|
| 892 |
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
| 893 |
)
|
| 894 |
|
| 895 |
+
def _create_openapi_resource(
|
| 896 |
+
self,
|
| 897 |
+
route: openapi.HTTPRoute,
|
| 898 |
+
name: str,
|
| 899 |
+
tags: set[str],
|
| 900 |
+
):
|
| 901 |
"""Creates and registers an OpenAPIResource with enhanced description."""
|
| 902 |
# Get a unique resource name
|
| 903 |
resource_name = self._get_unique_name(name, "resource")
|
|
|
|
| 921 |
uri=resource_uri,
|
| 922 |
name=resource_name,
|
| 923 |
description=enhanced_description,
|
| 924 |
+
tags=set(route.tags or []) | tags,
|
| 925 |
timeout=self._timeout,
|
| 926 |
)
|
| 927 |
|
|
|
|
| 942 |
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
| 943 |
)
|
| 944 |
|
| 945 |
+
def _create_openapi_template(
|
| 946 |
+
self,
|
| 947 |
+
route: openapi.HTTPRoute,
|
| 948 |
+
name: str,
|
| 949 |
+
tags: set[str],
|
| 950 |
+
):
|
| 951 |
"""Creates and registers an OpenAPIResourceTemplate with enhanced description."""
|
| 952 |
# Get a unique template name
|
| 953 |
template_name = self._get_unique_name(name, "resource_template")
|
|
|
|
| 1000 |
name=template_name,
|
| 1001 |
description=enhanced_description,
|
| 1002 |
parameters=template_params_schema,
|
| 1003 |
+
tags=set(route.tags or []) | tags,
|
| 1004 |
timeout=self._timeout,
|
| 1005 |
)
|
| 1006 |
|
tests/server/openapi/test_route_map_fn.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
import httpx
|
| 4 |
import pytest
|
| 5 |
|
| 6 |
-
from fastmcp.server.openapi import FastMCPOpenAPI, MCPType
|
| 7 |
|
| 8 |
|
| 9 |
@pytest.fixture
|
|
@@ -175,8 +175,6 @@ def test_route_map_fn_returns_none(sample_openapi_spec, http_client):
|
|
| 175 |
def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_client):
|
| 176 |
"""Test that route_map_fn is called for excluded routes and can rescue them."""
|
| 177 |
|
| 178 |
-
from fastmcp.server.openapi import RouteMap
|
| 179 |
-
|
| 180 |
# Exclude all admin routes
|
| 181 |
route_maps = [
|
| 182 |
RouteMap(
|
|
@@ -188,7 +186,7 @@ def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_clien
|
|
| 188 |
|
| 189 |
def track_calls_and_rescue(route, mcp_type):
|
| 190 |
"""Track which routes the function is called for and rescue some excluded routes."""
|
| 191 |
-
called_routes.append(route.path)
|
| 192 |
|
| 193 |
# Rescue the admin GET route by converting it to a tool
|
| 194 |
if route.path == "/admin/settings" and route.method == "GET":
|
|
@@ -205,10 +203,11 @@ def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_clien
|
|
| 205 |
)
|
| 206 |
|
| 207 |
# route_map_fn should now be called for all routes, including excluded admin routes
|
| 208 |
-
assert "/admin/settings" in called_routes
|
| 209 |
-
assert "/users" in called_routes
|
| 210 |
-
assert "/users/{id}" in called_routes
|
| 211 |
-
assert "/api/data" in called_routes
|
|
|
|
| 212 |
|
| 213 |
# The rescued admin GET route should now be a tool
|
| 214 |
tools = server._tool_manager._tools
|
|
@@ -296,7 +295,7 @@ def test_combined_route_map_fn_and_component_fn(sample_openapi_spec, http_client
|
|
| 296 |
|
| 297 |
def test_route_map_fn_signature_validation():
|
| 298 |
"""Test that route_map_fn has the correct signature."""
|
| 299 |
-
|
| 300 |
from fastmcp.utilities import openapi
|
| 301 |
|
| 302 |
# This is more of a type checking test
|
|
@@ -335,8 +334,6 @@ def test_component_fn_signature_validation():
|
|
| 335 |
def test_route_map_fn_can_rescue_excluded_routes(sample_openapi_spec, http_client):
|
| 336 |
"""Test that route_map_fn can rescue routes that were excluded by RouteMap."""
|
| 337 |
|
| 338 |
-
from fastmcp.server.openapi import RouteMap
|
| 339 |
-
|
| 340 |
# Exclude ALL routes by default
|
| 341 |
route_maps = [
|
| 342 |
RouteMap(mcp_type=MCPType.EXCLUDE) # Catch-all exclusion
|
|
|
|
| 3 |
import httpx
|
| 4 |
import pytest
|
| 5 |
|
| 6 |
+
from fastmcp.server.openapi import FastMCPOpenAPI, MCPType, RouteMap, RouteMapFn
|
| 7 |
|
| 8 |
|
| 9 |
@pytest.fixture
|
|
|
|
| 175 |
def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_client):
|
| 176 |
"""Test that route_map_fn is called for excluded routes and can rescue them."""
|
| 177 |
|
|
|
|
|
|
|
| 178 |
# Exclude all admin routes
|
| 179 |
route_maps = [
|
| 180 |
RouteMap(
|
|
|
|
| 186 |
|
| 187 |
def track_calls_and_rescue(route, mcp_type):
|
| 188 |
"""Track which routes the function is called for and rescue some excluded routes."""
|
| 189 |
+
called_routes.append((route.method, route.path))
|
| 190 |
|
| 191 |
# Rescue the admin GET route by converting it to a tool
|
| 192 |
if route.path == "/admin/settings" and route.method == "GET":
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
# route_map_fn should now be called for all routes, including excluded admin routes
|
| 206 |
+
assert ("GET", "/admin/settings") in called_routes
|
| 207 |
+
assert ("GET", "/users") in called_routes
|
| 208 |
+
assert ("GET", "/users/{id}") in called_routes
|
| 209 |
+
assert ("GET", "/api/data") in called_routes
|
| 210 |
+
assert ("POST", "/admin/settings") in called_routes
|
| 211 |
|
| 212 |
# The rescued admin GET route should now be a tool
|
| 213 |
tools = server._tool_manager._tools
|
|
|
|
| 295 |
|
| 296 |
def test_route_map_fn_signature_validation():
|
| 297 |
"""Test that route_map_fn has the correct signature."""
|
| 298 |
+
|
| 299 |
from fastmcp.utilities import openapi
|
| 300 |
|
| 301 |
# This is more of a type checking test
|
|
|
|
| 334 |
def test_route_map_fn_can_rescue_excluded_routes(sample_openapi_spec, http_client):
|
| 335 |
"""Test that route_map_fn can rescue routes that were excluded by RouteMap."""
|
| 336 |
|
|
|
|
|
|
|
| 337 |
# Exclude ALL routes by default
|
| 338 |
route_maps = [
|
| 339 |
RouteMap(mcp_type=MCPType.EXCLUDE) # Catch-all exclusion
|