From 5f8ad488733ba671435c47949d7cb6496397de56 Mon Sep 17 00:00:00 2001 From: Marc Jermaine Pontiveros <109291819+ww-jermaine@users.noreply.github.com> Date: Mon, 19 May 2025 11:22:54 +0000 Subject: [PATCH 1/4] Feature add support for fetching OpenAPI schema from a remote FastAPI server --- README.md | 24 +++++++++++++++++++++++ fastapi_mcp/server.py | 38 +++++++++++++++++++++++++++++------- tests/test_remote_openapi.py | 28 ++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 tests/test_remote_openapi.py diff --git a/README.md b/README.md index c5b2c24..0c54ce7 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,30 @@ mcp.mount() That's it! Your auto-generated MCP server is now available at `https://app.base.url/mcp`. +## Remote OpenAPI Schema + +You can configure FastApiMCP to fetch the OpenAPI schema from a remote FastAPI server by providing a custom httpx.AsyncClient and setting `fetch_openapi_from_remote=True`: + +```python +import httpx +from fastapi import FastAPI +from fastapi_mcp import FastApiMCP + +REMOTE_API_URL = "http://127.0.0.1:5000" +app = FastAPI() +client = httpx.AsyncClient(base_url=REMOTE_API_URL) + +mcp = FastApiMCP( + app, + name="MCP for Remote API", + http_client=client, + fetch_openapi_from_remote=True, +) +mcp.mount() +``` + +If `fetch_openapi_from_remote` is set and a custom client is provided, FastApiMCP will retrieve the OpenAPI schema from the remote server's `/openapi.json` endpoint instead of generating it locally. + ## Documentation, Examples and Advanced Usage FastAPI-MCP provides [comprehensive documentation](https://fastapi-mcp.tadata.com/). Additionaly, check out the [examples directory](examples) for code samples demonstrating these features in action. diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index f5c4fc6..a10206c 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -93,6 +93,14 @@ def __init__( """ ), ] = None, + fetch_openapi_from_remote: Annotated[ + bool, + Doc( + """ + If True and http_client is provided, fetch OpenAPI schema from remote /openapi.json instead of generating locally. + """ + ), + ] = False, include_operations: Annotated[ Optional[List[str]], Doc("List of operation IDs to include as MCP tools. Cannot be used with exclude_operations."), @@ -137,6 +145,7 @@ def __init__( self._include_tags = include_tags self._exclude_tags = exclude_tags self._auth_config = auth_config + self.fetch_openapi_from_remote = fetch_openapi_from_remote if self._auth_config: self._auth_config = self._auth_config.model_validate(self._auth_config) @@ -150,13 +159,28 @@ def __init__( self.setup_server() def setup_server(self) -> None: - openapi_schema = get_openapi( - title=self.fastapi.title, - version=self.fastapi.version, - openapi_version=self.fastapi.openapi_version, - description=self.fastapi.description, - routes=self.fastapi.routes, - ) + import asyncio + + openapi_schema = None + if self.fetch_openapi_from_remote and self._http_client: + + async def fetch_openapi(): + resp = await self._http_client.get("/openapi.json") + resp.raise_for_status() + return resp.json() + + try: + openapi_schema = asyncio.get_event_loop().run_until_complete(fetch_openapi()) + except Exception as e: + logger.error(f"Failed to fetch remote OpenAPI schema: {e}. Falling back to local generation.") + if openapi_schema is None: + openapi_schema = get_openapi( + title=self.fastapi.title, + version=self.fastapi.version, + openapi_version=self.fastapi.openapi_version, + description=self.fastapi.description, + routes=self.fastapi.routes, + ) all_tools, self.operation_map = convert_openapi_to_mcp_tools( openapi_schema, diff --git a/tests/test_remote_openapi.py b/tests/test_remote_openapi.py new file mode 100644 index 0000000..5cb508d --- /dev/null +++ b/tests/test_remote_openapi.py @@ -0,0 +1,28 @@ +import pytest +from fastapi import FastAPI +from fastapi_mcp import FastApiMCP + + +@pytest.mark.asyncio +def test_fetch_openapi_from_remote(monkeypatch): + app = FastAPI() + remote_openapi = {"openapi": "3.0.0", "info": {"title": "Remote", "version": "1.0.0"}, "paths": {}} + + class MockAsyncClient: + async def get(self, url): + class Resp: + def raise_for_status(self): + pass + + def json(self): + return remote_openapi + + return Resp() + + client = MockAsyncClient() + mcp = FastApiMCP(app, http_client=client, fetch_openapi_from_remote=True) + # The openapi schema should be fetched from remote + assert mcp.tools is not None + assert mcp.operation_map is not None + # The schema should match what the mock returned + assert mcp.operation_map == {} # since paths is empty From 2eef46df858a92f6deccbf98af9cea36ee4493c4 Mon Sep 17 00:00:00 2001 From: Marc Jermaine Pontiveros <109291819+ww-jermaine@users.noreply.github.com> Date: Tue, 27 May 2025 07:18:44 +0800 Subject: [PATCH 2/4] Update fastapi_mcp/server.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- fastapi_mcp/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index a10206c..209a179 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -145,7 +145,7 @@ def __init__( self._include_tags = include_tags self._exclude_tags = exclude_tags self._auth_config = auth_config - self.fetch_openapi_from_remote = fetch_openapi_from_remote + self._fetch_openapi_from_remote = fetch_openapi_from_remote if self._auth_config: self._auth_config = self._auth_config.model_validate(self._auth_config) From f943bc0555f10f2b8b16aac0f65f90ead7b8c3fd Mon Sep 17 00:00:00 2001 From: Marc Jermaine Pontiveros <109291819+ww-jermaine@users.noreply.github.com> Date: Tue, 27 May 2025 07:19:32 +0800 Subject: [PATCH 3/4] Update fastapi_mcp/server.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- fastapi_mcp/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index 209a179..2626c0b 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -165,7 +165,8 @@ def setup_server(self) -> None: if self.fetch_openapi_from_remote and self._http_client: async def fetch_openapi(): - resp = await self._http_client.get("/openapi.json") + openapi_url = getattr(self.fastapi, "openapi_url", "/openapi.json") + resp = await self._http_client.get(openapi_url) resp.raise_for_status() return resp.json() From dbecd5818124dd024267a83856c7f25862cd3eac Mon Sep 17 00:00:00 2001 From: Marc Jermaine Pontiveros <109291819+ww-jermaine@users.noreply.github.com> Date: Tue, 27 May 2025 07:19:51 +0800 Subject: [PATCH 4/4] Update tests/test_remote_openapi.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- tests/test_remote_openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_remote_openapi.py b/tests/test_remote_openapi.py index 5cb508d..e0ddc67 100644 --- a/tests/test_remote_openapi.py +++ b/tests/test_remote_openapi.py @@ -22,7 +22,7 @@ def json(self): client = MockAsyncClient() mcp = FastApiMCP(app, http_client=client, fetch_openapi_from_remote=True) # The openapi schema should be fetched from remote - assert mcp.tools is not None + assert mcp.tools == [] # tools should be an empty list since paths is empty assert mcp.operation_map is not None # The schema should match what the mock returned assert mcp.operation_map == {} # since paths is empty