mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-27 15:51:33 +02:00
Merge branch 'config-dicts' of https://github.com/jlowin/fastmcp into config-dicts
This commit is contained in:
commit
885f2fc4a1
14 changed files with 572 additions and 82 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue