Jeremiah Lowin commited on
Commit
dc0a2a3
·
1 Parent(s): 96618d0

Add fastapi conversion

Browse files
src/fastmcp/server/openapi.py CHANGED
@@ -13,18 +13,15 @@ from fastmcp.resources import Resource, ResourceTemplate
13
  from fastmcp.server.server import FastMCP
14
  from fastmcp.tools.base import Tool
15
  from fastmcp.utilities import openapi
16
- from fastmcp.utilities.func_metadata import (
17
- func_metadata as mcp_func_metadata,
18
- )
19
  from fastmcp.utilities.logging import get_logger
20
-
21
- # Re-export the formatter function for convenience
22
- from fastmcp.utilities.openapi import format_description_with_responses
 
23
 
24
  logger = get_logger(__name__)
25
 
26
-
27
- # HTTP Methods as a Literal for type checking
28
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
29
 
30
 
@@ -98,6 +95,13 @@ def _determine_route_type(
98
  return RouteType.TOOL
99
 
100
 
 
 
 
 
 
 
 
101
  class OpenAPITool(Tool):
102
  """Tool implementation for OpenAPI endpoints."""
103
 
@@ -447,7 +451,6 @@ class FastMCPOpenAPI(FastMCP):
447
  client: httpx.AsyncClient,
448
  name: str | None = None,
449
  route_maps: list[RouteMap] | None = None,
450
- default_mime_type: str = "application/json",
451
  **settings: Any,
452
  ):
453
  """
@@ -464,7 +467,6 @@ class FastMCPOpenAPI(FastMCP):
464
  super().__init__(name=name or "OpenAPI FastMCP", **settings)
465
 
466
  self._client = client
467
- self._default_mime_type = default_mime_type
468
 
469
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
470
 
@@ -547,7 +549,6 @@ class FastMCPOpenAPI(FastMCP):
547
  uri=resource_uri,
548
  name=resource_name,
549
  description=enhanced_description,
550
- mime_type=self._default_mime_type,
551
  )
552
  # Register the resource by directly assigning to the resources dictionary
553
  self._resource_manager._resources[str(resource.uri)] = resource
@@ -621,53 +622,3 @@ class FastMCPOpenAPI(FastMCP):
621
  pass
622
 
623
  return result
624
-
625
-
626
- # Function metadata utility
627
- def func_metadata(fn):
628
- """Function to generate metadata for a function."""
629
- return mcp_func_metadata(fn)
630
-
631
-
632
- # Placeholder function to provide function metadata
633
- async def _openapi_passthrough(*args, **kwargs):
634
- """Placeholder function for OpenAPI endpoints."""
635
- # This is kept for metadata generation purposes
636
- pass
637
-
638
-
639
- def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
640
- """
641
- Combines parameter and request body schemas into a single schema.
642
-
643
- Args:
644
- route: HTTPRoute object
645
-
646
- Returns:
647
- Combined schema dictionary
648
- """
649
- properties = {}
650
- required = []
651
-
652
- # Add path parameters
653
- for param in route.parameters:
654
- if param.required:
655
- required.append(param.name)
656
- properties[param.name] = param.schema_
657
-
658
- # Add request body if it exists
659
- if route.request_body and route.request_body.content_schema:
660
- # For now, just use the first content type's schema
661
- content_type = next(iter(route.request_body.content_schema))
662
- body_schema = route.request_body.content_schema[content_type]
663
- body_props = body_schema.get("properties", {})
664
- for prop_name, prop_schema in body_props.items():
665
- properties[prop_name] = prop_schema
666
- if route.request_body.required:
667
- required.extend(body_schema.get("required", []))
668
-
669
- return {
670
- "type": "object",
671
- "properties": properties,
672
- "required": required,
673
- }
 
13
  from fastmcp.server.server import FastMCP
14
  from fastmcp.tools.base import Tool
15
  from fastmcp.utilities import openapi
16
+ from fastmcp.utilities.func_metadata import func_metadata
 
 
17
  from fastmcp.utilities.logging import get_logger
18
+ from fastmcp.utilities.openapi import (
19
+ _combine_schemas,
20
+ format_description_with_responses,
21
+ )
22
 
23
  logger = get_logger(__name__)
24
 
 
 
25
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
26
 
27
 
 
95
  return RouteType.TOOL
96
 
97
 
98
+ # Placeholder function to provide function metadata
99
+ async def _openapi_passthrough(*args, **kwargs):
100
+ """Placeholder function for OpenAPI endpoints."""
101
+ # This is kept for metadata generation purposes
102
+ pass
103
+
104
+
105
  class OpenAPITool(Tool):
106
  """Tool implementation for OpenAPI endpoints."""
107
 
 
451
  client: httpx.AsyncClient,
452
  name: str | None = None,
453
  route_maps: list[RouteMap] | None = None,
 
454
  **settings: Any,
455
  ):
456
  """
 
467
  super().__init__(name=name or "OpenAPI FastMCP", **settings)
468
 
469
  self._client = client
 
470
 
471
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
472
 
 
549
  uri=resource_uri,
550
  name=resource_name,
551
  description=enhanced_description,
 
552
  )
553
  # Register the resource by directly assigning to the resources dictionary
554
  self._resource_manager._resources[str(resource.uri)] = resource
 
622
  pass
623
 
624
  return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/server.py CHANGED
@@ -14,8 +14,10 @@ from itertools import chain
14
  from typing import TYPE_CHECKING, Any, Generic, Literal
15
 
16
  import anyio
 
17
  import pydantic_core
18
  import uvicorn
 
19
  from mcp.server.lowlevel.helper_types import ReadResourceContents
20
  from mcp.server.lowlevel.server import LifespanResultT
21
  from mcp.server.lowlevel.server import Server as MCPServer
@@ -52,6 +54,7 @@ from fastmcp.utilities.types import Image
52
  if TYPE_CHECKING:
53
  from fastmcp.clients.base import BaseClient
54
  from fastmcp.server.context import Context
 
55
  from fastmcp.server.proxy import FastMCPProxy
56
 
57
  logger = get_logger(__name__)
@@ -565,6 +568,36 @@ class FastMCP(Generic[LifespanResultT]):
565
 
566
  return await FastMCPProxy.from_client(client=client, **settings)
567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
 
569
  def _convert_to_content(
570
  result: Any,
 
14
  from typing import TYPE_CHECKING, Any, Generic, Literal
15
 
16
  import anyio
17
+ import httpx
18
  import pydantic_core
19
  import uvicorn
20
+ from fastapi import FastAPI
21
  from mcp.server.lowlevel.helper_types import ReadResourceContents
22
  from mcp.server.lowlevel.server import LifespanResultT
23
  from mcp.server.lowlevel.server import Server as MCPServer
 
54
  if TYPE_CHECKING:
55
  from fastmcp.clients.base import BaseClient
56
  from fastmcp.server.context import Context
57
+ from fastmcp.server.openapi import FastMCPOpenAPI
58
  from fastmcp.server.proxy import FastMCPProxy
59
 
60
  logger = get_logger(__name__)
 
568
 
569
  return await FastMCPProxy.from_client(client=client, **settings)
570
 
571
+ @classmethod
572
+ def from_openapi(
573
+ cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
574
+ ) -> "FastMCPOpenAPI":
575
+ """
576
+ Create a FastMCP server from an OpenAPI specification.
577
+ """
578
+ from .openapi import FastMCPOpenAPI
579
+
580
+ return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
581
+
582
+ @classmethod
583
+ def from_fastapi(
584
+ cls, app: FastAPI, name: str | None = None, **settings: Any
585
+ ) -> "FastMCPOpenAPI":
586
+ """
587
+ Create a FastMCP server from a FastAPI application.
588
+ """
589
+ from .openapi import FastMCPOpenAPI
590
+
591
+ client = httpx.AsyncClient(
592
+ transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
593
+ )
594
+
595
+ name = name or app.title
596
+
597
+ return FastMCPOpenAPI(
598
+ openapi_spec=app.openapi(), client=client, name=name, **settings
599
+ )
600
+
601
 
602
  def _convert_to_content(
603
  result: Any,
src/fastmcp/utilities/openapi.py CHANGED
@@ -16,6 +16,8 @@ from openapi_pydantic import (
16
  )
17
  from pydantic import BaseModel, Field, ValidationError
18
 
 
 
19
  logger = logging.getLogger(__name__)
20
 
21
  # --- Intermediate Representation (IR) Definition ---
@@ -756,3 +758,40 @@ def format_description_with_responses(
756
  )
757
 
758
  return "\n".join(desc_parts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  )
17
  from pydantic import BaseModel, Field, ValidationError
18
 
19
+ from fastmcp.utilities import openapi
20
+
21
  logger = logging.getLogger(__name__)
22
 
23
  # --- Intermediate Representation (IR) Definition ---
 
758
  )
759
 
760
  return "\n".join(desc_parts)
761
+
762
+
763
+ def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
764
+ """
765
+ Combines parameter and request body schemas into a single schema.
766
+
767
+ Args:
768
+ route: HTTPRoute object
769
+
770
+ Returns:
771
+ Combined schema dictionary
772
+ """
773
+ properties = {}
774
+ required = []
775
+
776
+ # Add path parameters
777
+ for param in route.parameters:
778
+ if param.required:
779
+ required.append(param.name)
780
+ properties[param.name] = param.schema_
781
+
782
+ # Add request body if it exists
783
+ if route.request_body and route.request_body.content_schema:
784
+ # For now, just use the first content type's schema
785
+ content_type = next(iter(route.request_body.content_schema))
786
+ body_schema = route.request_body.content_schema[content_type]
787
+ body_props = body_schema.get("properties", {})
788
+ for prop_name, prop_schema in body_props.items():
789
+ properties[prop_name] = prop_schema
790
+ if route.request_body.required:
791
+ required.extend(body_schema.get("required", []))
792
+
793
+ return {
794
+ "type": "object",
795
+ "properties": properties,
796
+ "required": required,
797
+ }
tests/server/test_openapi.py CHANGED
@@ -34,7 +34,7 @@ def users_db() -> dict[int, User]:
34
 
35
  @pytest.fixture
36
  def fastapi_app(users_db: dict[int, User]) -> FastAPI:
37
- app = FastAPI(name="Test App")
38
 
39
  @app.get("/users")
40
  async def get_users() -> list[User]:
@@ -98,6 +98,20 @@ async def test_create_openapi_server(
98
  assert server.name == "Test App"
99
 
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  class TestTools:
102
  async def test_list_tools(self, fastmcp_server: FastMCPOpenAPI):
103
  """
 
34
 
35
  @pytest.fixture
36
  def fastapi_app(users_db: dict[int, User]) -> FastAPI:
37
+ app = FastAPI(title="FastAPI App")
38
 
39
  @app.get("/users")
40
  async def get_users() -> list[User]:
 
98
  assert server.name == "Test App"
99
 
100
 
101
+ async def test_create_openapi_server_classmethod(
102
+ fastapi_app: FastAPI, api_client: httpx.AsyncClient
103
+ ):
104
+ server = FastMCP.from_openapi(openapi_spec=fastapi_app.openapi(), client=api_client)
105
+ assert isinstance(server, FastMCPOpenAPI)
106
+ assert server.name == "OpenAPI FastMCP"
107
+
108
+
109
+ async def test_create_fastapi_server_classmethod(fastapi_app: FastAPI):
110
+ server = FastMCP.from_fastapi(fastapi_app)
111
+ assert isinstance(server, FastMCPOpenAPI)
112
+ assert server.name == "FastAPI App"
113
+
114
+
115
  class TestTools:
116
  async def test_list_tools(self, fastmcp_server: FastMCPOpenAPI):
117
  """