mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
Add fastapi conversion
This commit is contained in:
parent
9ca99d5809
commit
8720a78747
4 changed files with 99 additions and 62 deletions
|
|
@ -13,18 +13,15 @@ from fastmcp.resources import Resource, ResourceTemplate
|
|||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.tools.base import Tool
|
||||
from fastmcp.utilities import openapi
|
||||
from fastmcp.utilities.func_metadata import (
|
||||
func_metadata as mcp_func_metadata,
|
||||
)
|
||||
from fastmcp.utilities.func_metadata import func_metadata
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
# Re-export the formatter function for convenience
|
||||
from fastmcp.utilities.openapi import format_description_with_responses
|
||||
from fastmcp.utilities.openapi import (
|
||||
_combine_schemas,
|
||||
format_description_with_responses,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# HTTP Methods as a Literal for type checking
|
||||
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
|
||||
|
||||
|
||||
|
|
@ -98,6 +95,13 @@ def _determine_route_type(
|
|||
return RouteType.TOOL
|
||||
|
||||
|
||||
# Placeholder function to provide function metadata
|
||||
async def _openapi_passthrough(*args, **kwargs):
|
||||
"""Placeholder function for OpenAPI endpoints."""
|
||||
# This is kept for metadata generation purposes
|
||||
pass
|
||||
|
||||
|
||||
class OpenAPITool(Tool):
|
||||
"""Tool implementation for OpenAPI endpoints."""
|
||||
|
||||
|
|
@ -447,7 +451,6 @@ class FastMCPOpenAPI(FastMCP):
|
|||
client: httpx.AsyncClient,
|
||||
name: str | None = None,
|
||||
route_maps: list[RouteMap] | None = None,
|
||||
default_mime_type: str = "application/json",
|
||||
**settings: Any,
|
||||
):
|
||||
"""
|
||||
|
|
@ -464,7 +467,6 @@ class FastMCPOpenAPI(FastMCP):
|
|||
super().__init__(name=name or "OpenAPI FastMCP", **settings)
|
||||
|
||||
self._client = client
|
||||
self._default_mime_type = default_mime_type
|
||||
|
||||
http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
|
||||
|
||||
|
|
@ -547,7 +549,6 @@ class FastMCPOpenAPI(FastMCP):
|
|||
uri=resource_uri,
|
||||
name=resource_name,
|
||||
description=enhanced_description,
|
||||
mime_type=self._default_mime_type,
|
||||
)
|
||||
# Register the resource by directly assigning to the resources dictionary
|
||||
self._resource_manager._resources[str(resource.uri)] = resource
|
||||
|
|
@ -621,53 +622,3 @@ class FastMCPOpenAPI(FastMCP):
|
|||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Function metadata utility
|
||||
def func_metadata(fn):
|
||||
"""Function to generate metadata for a function."""
|
||||
return mcp_func_metadata(fn)
|
||||
|
||||
|
||||
# Placeholder function to provide function metadata
|
||||
async def _openapi_passthrough(*args, **kwargs):
|
||||
"""Placeholder function for OpenAPI endpoints."""
|
||||
# This is kept for metadata generation purposes
|
||||
pass
|
||||
|
||||
|
||||
def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
|
||||
"""
|
||||
Combines parameter and request body schemas into a single schema.
|
||||
|
||||
Args:
|
||||
route: HTTPRoute object
|
||||
|
||||
Returns:
|
||||
Combined schema dictionary
|
||||
"""
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
# Add path parameters
|
||||
for param in route.parameters:
|
||||
if param.required:
|
||||
required.append(param.name)
|
||||
properties[param.name] = param.schema_
|
||||
|
||||
# Add request body if it exists
|
||||
if route.request_body and route.request_body.content_schema:
|
||||
# For now, just use the first content type's schema
|
||||
content_type = next(iter(route.request_body.content_schema))
|
||||
body_schema = route.request_body.content_schema[content_type]
|
||||
body_props = body_schema.get("properties", {})
|
||||
for prop_name, prop_schema in body_props.items():
|
||||
properties[prop_name] = prop_schema
|
||||
if route.request_body.required:
|
||||
required.extend(body_schema.get("required", []))
|
||||
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ from itertools import chain
|
|||
from typing import TYPE_CHECKING, Any, Generic, Literal
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
import pydantic_core
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||
from mcp.server.lowlevel.server import LifespanResultT
|
||||
from mcp.server.lowlevel.server import Server as MCPServer
|
||||
|
|
@ -52,6 +54,7 @@ from fastmcp.utilities.types import Image
|
|||
if TYPE_CHECKING:
|
||||
from fastmcp.clients.base import BaseClient
|
||||
from fastmcp.server.context import Context
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
from fastmcp.server.proxy import FastMCPProxy
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -565,6 +568,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
return await FastMCPProxy.from_client(client=client, **settings)
|
||||
|
||||
@classmethod
|
||||
def from_openapi(
|
||||
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
|
||||
) -> "FastMCPOpenAPI":
|
||||
"""
|
||||
Create a FastMCP server from an OpenAPI specification.
|
||||
"""
|
||||
from .openapi import FastMCPOpenAPI
|
||||
|
||||
return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
|
||||
|
||||
@classmethod
|
||||
def from_fastapi(
|
||||
cls, app: FastAPI, name: str | None = None, **settings: Any
|
||||
) -> "FastMCPOpenAPI":
|
||||
"""
|
||||
Create a FastMCP server from a FastAPI application.
|
||||
"""
|
||||
from .openapi import FastMCPOpenAPI
|
||||
|
||||
client = httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
|
||||
)
|
||||
|
||||
name = name or app.title
|
||||
|
||||
return FastMCPOpenAPI(
|
||||
openapi_spec=app.openapi(), client=client, name=name, **settings
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_content(
|
||||
result: Any,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ from openapi_pydantic import (
|
|||
)
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from fastmcp.utilities import openapi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Intermediate Representation (IR) Definition ---
|
||||
|
|
@ -756,3 +758,40 @@ def format_description_with_responses(
|
|||
)
|
||||
|
||||
return "\n".join(desc_parts)
|
||||
|
||||
|
||||
def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
|
||||
"""
|
||||
Combines parameter and request body schemas into a single schema.
|
||||
|
||||
Args:
|
||||
route: HTTPRoute object
|
||||
|
||||
Returns:
|
||||
Combined schema dictionary
|
||||
"""
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
# Add path parameters
|
||||
for param in route.parameters:
|
||||
if param.required:
|
||||
required.append(param.name)
|
||||
properties[param.name] = param.schema_
|
||||
|
||||
# Add request body if it exists
|
||||
if route.request_body and route.request_body.content_schema:
|
||||
# For now, just use the first content type's schema
|
||||
content_type = next(iter(route.request_body.content_schema))
|
||||
body_schema = route.request_body.content_schema[content_type]
|
||||
body_props = body_schema.get("properties", {})
|
||||
for prop_name, prop_schema in body_props.items():
|
||||
properties[prop_name] = prop_schema
|
||||
if route.request_body.required:
|
||||
required.extend(body_schema.get("required", []))
|
||||
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ def users_db() -> dict[int, User]:
|
|||
|
||||
@pytest.fixture
|
||||
def fastapi_app(users_db: dict[int, User]) -> FastAPI:
|
||||
app = FastAPI(name="Test App")
|
||||
app = FastAPI(title="FastAPI App")
|
||||
|
||||
@app.get("/users")
|
||||
async def get_users() -> list[User]:
|
||||
|
|
@ -98,6 +98,20 @@ async def test_create_openapi_server(
|
|||
assert server.name == "Test App"
|
||||
|
||||
|
||||
async def test_create_openapi_server_classmethod(
|
||||
fastapi_app: FastAPI, api_client: httpx.AsyncClient
|
||||
):
|
||||
server = FastMCP.from_openapi(openapi_spec=fastapi_app.openapi(), client=api_client)
|
||||
assert isinstance(server, FastMCPOpenAPI)
|
||||
assert server.name == "OpenAPI FastMCP"
|
||||
|
||||
|
||||
async def test_create_fastapi_server_classmethod(fastapi_app: FastAPI):
|
||||
server = FastMCP.from_fastapi(fastapi_app)
|
||||
assert isinstance(server, FastMCPOpenAPI)
|
||||
assert server.name == "FastAPI App"
|
||||
|
||||
|
||||
class TestTools:
|
||||
async def test_list_tools(self, fastmcp_server: FastMCPOpenAPI):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue