Merge branch 'config-dicts' of https://github.com/jlowin/fastmcp into config-dicts

This commit is contained in:
Jeremiah Lowin 2025-05-20 17:46:36 -04:00
commit 885f2fc4a1
14 changed files with 572 additions and 82 deletions

View file

@ -306,7 +306,29 @@ def create_streamable_http_app(
async def handle_streamable_http(
scope: Scope, receive: Receive, send: Send
) -> None:
await session_manager.handle_request(scope, receive, send)
try:
await session_manager.handle_request(scope, receive, send)
except RuntimeError as e:
if str(e) == "Task group is not initialized. Make sure to use run().":
logger.error(
f"Original RuntimeError from mcp library: {e}", exc_info=True
)
new_error_message = (
"FastMCP's StreamableHTTPSessionManager task group was not initialized. "
"This commonly occurs when the FastMCP application's lifespan is not "
"passed to the parent ASGI application (e.g., FastAPI or Starlette). "
"Please ensure you are setting `lifespan=mcp_app.lifespan` in your "
"parent app's constructor, where `mcp_app` is the application instance "
"returned by `fastmcp_instance.http_app()`. \\n"
"For more details, see the FastMCP ASGI integration documentation: "
"https://gofastmcp.com/deployment/asgi"
)
# Raise a new RuntimeError that includes the original error's message
# for full context, but leads with the more helpful guidance.
raise RuntimeError(f"{new_error_message}\\nOriginal error: {e}") from e
else:
# Re-raise other RuntimeErrors if they don't match the specific message
raise
# Get auth middleware and routes
auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(

View file

@ -47,7 +47,7 @@ class RouteType(enum.Enum):
class RouteMap:
"""Mapping configuration for HTTP routes to FastMCP component types."""
methods: list[HttpMethod]
methods: list[HttpMethod] | Literal["*"]
pattern: Pattern[str] | str
route_type: RouteType
@ -86,7 +86,7 @@ def _determine_route_type(
# Check mappings in priority order (first match wins)
for route_map in mappings:
# Check if the HTTP method matches
if route.method in route_map.methods:
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)

View file

@ -62,7 +62,7 @@ from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from fastmcp.client import Client
from fastmcp.client.transports import ClientTransport
from fastmcp.server.openapi import FastMCPOpenAPI
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
from fastmcp.server.proxy import FastMCPProxy
logger = get_logger(__name__)
@ -1082,24 +1082,59 @@ class FastMCP(Generic[LifespanResultT]):
@classmethod
def from_openapi(
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
cls,
openapi_spec: dict[str, Any],
client: httpx.AsyncClient,
route_maps: list[RouteMap] | None = None,
all_routes_as_tools: bool = False,
**settings: Any,
) -> FastMCPOpenAPI:
"""
Create a FastMCP server from an OpenAPI specification.
"""
from .openapi import FastMCPOpenAPI
from .openapi import FastMCPOpenAPI, RouteMap, RouteType
return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
if all_routes_as_tools and route_maps:
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
elif all_routes_as_tools:
route_maps = [
RouteMap(
methods="*",
pattern=r".*",
route_type=RouteType.TOOL,
)
]
return FastMCPOpenAPI(
openapi_spec=openapi_spec,
client=client,
route_maps=route_maps,
**settings,
)
@classmethod
def from_fastapi(
cls, app: Any, name: str | None = None, **settings: Any
cls,
app: Any,
name: str | None = None,
route_maps: list[RouteMap] | None = None,
all_routes_as_tools: bool = False,
**settings: Any,
) -> FastMCPOpenAPI:
"""
Create a FastMCP server from a FastAPI application.
"""
from .openapi import FastMCPOpenAPI
from .openapi import FastMCPOpenAPI, RouteMap, RouteType
if all_routes_as_tools and route_maps:
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
elif all_routes_as_tools:
route_maps = [
RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
]
client = httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
@ -1108,7 +1143,11 @@ class FastMCP(Generic[LifespanResultT]):
name = name or app.title
return FastMCPOpenAPI(
openapi_spec=app.openapi(), client=client, name=name, **settings
openapi_spec=app.openapi(),
client=client,
name=name,
route_maps=route_maps,
**settings,
)
@classmethod

View file

@ -71,7 +71,7 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No
@contextmanager
def run_server_in_process(
server_fn: Callable[[str, int], None], *args
server_fn: Callable[..., None], *args
) -> Generator[str, None, None]:
"""
Context manager that runs a Starlette app in a separate process and returns the
@ -109,7 +109,11 @@ def run_server_in_process(
yield f"http://{host}:{port}"
proc.kill()
proc.join(timeout=2)
proc.terminate()
proc.join(timeout=5)
if proc.is_alive():
raise RuntimeError("Server process failed to terminate")
# If it's still alive, then force kill it
proc.kill()
proc.join(timeout=2)
if proc.is_alive():
raise RuntimeError("Server process failed to terminate even after kill")