133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
"""Route mapping logic for OpenAPI operations."""
|
|
|
|
import enum
|
|
import re
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from re import Pattern
|
|
from typing import TYPE_CHECKING, Literal
|
|
|
|
if TYPE_CHECKING:
|
|
from .components import (
|
|
OpenAPIResource,
|
|
OpenAPIResourceTemplate,
|
|
OpenAPITool,
|
|
)
|
|
# Import from our new utilities
|
|
from fastmcp.experimental.utilities.openapi import HttpMethod, HTTPRoute
|
|
from fastmcp.utilities.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
# Type definitions for the mapping functions
|
|
RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
|
|
ComponentFn = Callable[
|
|
[
|
|
HTTPRoute,
|
|
"OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate",
|
|
],
|
|
None,
|
|
]
|
|
|
|
|
|
class MCPType(enum.Enum):
|
|
"""Type of FastMCP component to create from a route.
|
|
|
|
Enum values:
|
|
TOOL: Convert the route to a callable Tool
|
|
RESOURCE: Convert the route to a Resource (typically GET endpoints)
|
|
RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
|
|
EXCLUDE: Exclude the route from being converted to any MCP component
|
|
"""
|
|
|
|
TOOL = "TOOL"
|
|
RESOURCE = "RESOURCE"
|
|
RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
|
|
# PROMPT = "PROMPT"
|
|
EXCLUDE = "EXCLUDE"
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class RouteMap:
|
|
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
|
|
|
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
|
pattern: Pattern[str] | str = field(default=r".*")
|
|
|
|
tags: set[str] = field(
|
|
default_factory=set,
|
|
metadata={"description": "A set of tags to match. All tags must match."},
|
|
)
|
|
mcp_type: MCPType = field(
|
|
metadata={"description": "The type of FastMCP component to create."},
|
|
)
|
|
mcp_tags: set[str] = field(
|
|
default_factory=set,
|
|
metadata={
|
|
"description": "A set of tags to apply to the generated FastMCP component."
|
|
},
|
|
)
|
|
|
|
|
|
# Default route mapping: all routes become tools.
|
|
# Users can provide custom route_maps to override this behavior.
|
|
DEFAULT_ROUTE_MAPPINGS = [
|
|
RouteMap(mcp_type=MCPType.TOOL),
|
|
]
|
|
|
|
|
|
def _determine_route_type(
|
|
route: HTTPRoute,
|
|
mappings: list[RouteMap],
|
|
) -> RouteMap:
|
|
"""
|
|
Determines the FastMCP component type based on the route and mappings.
|
|
|
|
Args:
|
|
route: HTTPRoute object
|
|
mappings: List of RouteMap objects in priority order
|
|
|
|
Returns:
|
|
The RouteMap that matches the route, or a catchall "Tool" RouteMap if no match is found.
|
|
"""
|
|
# Check mappings in priority order (first match wins)
|
|
for route_map in mappings:
|
|
# Check if the HTTP method matches
|
|
if route_map.methods == "*" or route.method in route_map.methods:
|
|
# Handle both string patterns and compiled Pattern objects
|
|
if isinstance(route_map.pattern, Pattern):
|
|
pattern_matches = route_map.pattern.search(route.path)
|
|
else:
|
|
pattern_matches = re.search(route_map.pattern, route.path)
|
|
|
|
if pattern_matches:
|
|
# Check if tags match (if specified)
|
|
# If route_map.tags is empty, tags are not matched
|
|
# If route_map.tags is non-empty, all tags must be present in route.tags (AND condition)
|
|
if route_map.tags:
|
|
route_tags_set = set(route.tags or [])
|
|
if not route_map.tags.issubset(route_tags_set):
|
|
# Tags don't match, continue to next mapping
|
|
continue
|
|
|
|
# We know mcp_type is not None here due to post_init validation
|
|
assert route_map.mcp_type is not None
|
|
logger.debug(
|
|
f"Route {route.method} {route.path} mapped to {route_map.mcp_type.name}"
|
|
)
|
|
return route_map
|
|
|
|
# Default fallback
|
|
return RouteMap(mcp_type=MCPType.TOOL)
|
|
|
|
|
|
# Export public symbols
|
|
__all__ = [
|
|
"MCPType",
|
|
"RouteMap",
|
|
"RouteMapFn",
|
|
"ComponentFn",
|
|
"DEFAULT_ROUTE_MAPPINGS",
|
|
"_determine_route_type",
|
|
]
|