diff --git a/docs/patterns/fastapi.mdx b/docs/patterns/fastapi.mdx index 45e9d2e78..1bbfd6231 100644 --- a/docs/patterns/fastapi.mdx +++ b/docs/patterns/fastapi.mdx @@ -1,114 +1,120 @@ --- title: FastAPI Integration sidebarTitle: FastAPI -description: Automatically create FastMCP servers directly from FastAPI applications. +description: Generate MCP servers from FastAPI apps icon: square-bolt --- -If you build your APIs using the popular [FastAPI](https://fastapi.tiangolo.com/) framework, FastMCP offers a seamless way to expose your FastAPI application as an MCP server. This leverages the OpenAPI integration internally but simplifies the setup significantly. -## The Goal: FastAPI App -> MCP Server +FastMCP can automatically convert FastAPI applications into MCP servers. -FastAPI automatically generates an OpenAPI specification for your application. FastMCP uses this built-in capability to create an MCP server that mirrors your API routes. + +FastMCP does *not* include FastAPI as a dependency; you must install it separately to run these examples. + -- FastAPI path operations (`@app.get`, `@app.post`, etc.) become MCP tools, resources, or templates. -- Pydantic models used in FastAPI for request/response validation are used to generate MCP schemas. -- Communication happens directly in memory, making it very efficient. -## Creating from FastAPI App +```python {2, 22, 25} +from fastapi import FastAPI +from fastmcp import FastMCP -Use the `FastMCP.from_fastapi()` class method. You only need your FastAPI `app` instance. + +# A FastAPI app +app = FastAPI() + +@app.get("/items") +def list_items(): + return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}] + +@app.get("/items/{item_id}") +def get_item(item_id: int): + return {"id": item_id, "name": f"Item {item_id}"} + +@app.post("/items") +def create_item(name: str): + return {"id": 3, "name": name} + + +# Create an MCP server from your FastAPI app +mcp = FastMCP.from_fastapi(app=app) + +if __name__ == "__main__": + mcp.run() # Start the MCP server +``` + +## Route Mapping + +By default, FastMCP will map FastAPI routes to MCP components according to the following rules: + +| FastAPI Route Type | FastAPI Example | MCP Component | Notes | +|--------------------|--------------|---------|-------| +| GET without path params | `@app.get("/stats")` | Resource | Simple resources for fetching data | +| GET with path params | `@app.get("/users/{id}")` | Resource Template | Path parameters become template parameters | +| POST, PUT, DELETE, etc. | `@app.post("/users")` | Tool | Operations that modify data | + +For more details on route mapping or custom mapping rules, see the [OpenAPI integration documentation](/patterns/openapi#route-mapping); FastMCP uses the same mapping rules for both FastAPI and OpenAPI integrations. + +## Complete Example + +Here's a more detailed example with a data model: ```python import asyncio -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from pydantic import BaseModel -from fastmcp import FastMCP, Client # Import FastMCP and Client - -# 1. Define your FastAPI application -api_app = FastAPI(title="MyFastAPIApp") +from fastmcp import FastMCP, Client +# Define your Pydantic model class Item(BaseModel): name: str price: float - is_offer: bool | None = None -@api_app.get("/") -def read_root(): - return {"Hello": "World"} +# Create your FastAPI app +app = FastAPI() +items = {} # In-memory database -@api_app.get("/items/{item_id}") # -> Resource Template -def read_item(item_id: int, q: str | None = None): - # This will become resource://openapi/read_item_items__item_id__get/{item_id} - return {"item_id": item_id, "q": q, "description": f"Details for item {item_id}"} +@app.get("/items") +def list_items(): + """List all items""" + return list(items.values()) -@api_app.post("/items/") # -> Tool +@app.get("/items/{item_id}") +def get_item(item_id: int): + """Get item by ID""" + if item_id not in items: + raise HTTPException(404, "Item not found") + return items[item_id] + +@app.post("/items") def create_item(item: Item): - # This will become the 'create_item_items__post' tool - print(f"Creating item: {item.name}") - return {"item_name": item.name, "status": "created"} + """Create a new item""" + item_id = len(items) + 1 + items[item_id] = {"id": item_id, **item.model_dump()} + return items[item_id] -# 2. Create the FastMCP server directly from the FastAPI app -# This is an async class method -async def create_mcp_server_from_fastapi(): - mcp_server = await FastMCP.from_fastapi( - app=api_app, - name="FastAPI_MCP_Bridge" # Optional name for the MCP server - ) - return mcp_server - -# 3. (Example) Run the MCP server and test with an in-memory client -async def run_and_test(): - server = await create_mcp_server_from_fastapi() - print(f"Created MCP server '{server.name}' from FastAPI app '{api_app.title}'") - - # List discovered components - tools = await server.list_tools() - templates = await server.list_resource_templates() - print("Discovered Tools:", [t.name for t in tools]) - print("Discovered Templates:", [t.uriTemplate for t in templates]) - - # Test using an in-memory client - client = Client(server) # Uses FastMCPTransport - async with client: - # Call the tool derived from POST /items/ - create_result = await client.call_tool( - "create_item_items__post", - {"name": "MCP Special", "price": 99.99} # Pydantic model fields become args - ) - print("Create Item Tool Result:", create_result[0].text) # JSON string - - # Read the resource derived from GET /items/{item_id} - read_result = await client.read_resource( - "resource://openapi/read_item_items__item_id__get/42" # Match template URI - ) - print("Read Item Resource Result:", read_result[0].text) # JSON string - - # In a real scenario, you might run the MCP server via stdio or sse - # print("Running MCP server via stdio...") - # server.run() +# Test your MCP server with a client +async def test(): + # Create MCP server from FastAPI app + mcp = await FastMCP.from_fastapi(app=app) + + # List the components that were created + tools = await mcp.list_tools() + resources = await mcp.list_resources() + templates = await mcp.list_resource_templates() + + print(f"Generated {len(tools)} tools") + print(f"Generated {len(resources)} resources") + print(f"Generated {len(templates)} templates") + + # In a real scenario, you would run the server: + # mcp.run() if __name__ == "__main__": - # Requires fastapi, uvicorn, httpx: - # uv pip install "fastapi[all]" httpx - try: - asyncio.run(run_and_test()) - except ImportError as e: - print(f"Error: {e}. Please install required packages: uv pip install \"fastapi[all]\" httpx") - -# Example Output might include: -# Created MCP server 'FastAPI_MCP_Bridge' from FastAPI app 'MyFastAPIApp' -# Discovered Tools: ['read_root___get', 'create_item_items__post'] -# Discovered Templates: ['resource://openapi/read_item_items__item_id__get/{item_id}'] -# Create Item Tool Result: {"item_name": "MCP Special", "status": "created"} -# Read Item Resource Result: {"item_id": 42, "q": null, "description": "Details for item 42"} + asyncio.run(test()) ``` -### How it Works Internally +## Benefits -1. **OpenAPI Generation**: `from_fastapi` asks the FastAPI `app` for its OpenAPI schema dictionary (`app.openapi()`). -2. **In-Memory Client**: It creates an `httpx.AsyncClient` configured with an `ASGITransport`. This special transport allows `httpx` to call the FastAPI application directly in memory without needing a running web server process. -3. **OpenAPI Integration**: It calls `FastMCP.from_openapi()`, passing the generated schema and the in-memory `httpx` client. -4. **MCP Server Creation**: The standard OpenAPI integration logic then proceeds to parse the schema and create the `Tool`, `Resource`, and `ResourceTemplate` components that wrap calls to the in-memory FastAPI app. - -This provides a highly efficient way to expose your FastAPI logic through the MCP protocol, leveraging FastAPI's routing, dependency injection, and validation features. \ No newline at end of file +- **Leverage existing FastAPI apps** - No need to rewrite your API logic +- **Schema reuse** - FastAPI's Pydantic models and validation are inherited +- **Full feature support** - Works with FastAPI's authentication, dependencies, etc. +- **ASGI transport** - Direct communication without additional HTTP overhead diff --git a/docs/patterns/openapi.mdx b/docs/patterns/openapi.mdx index 1767700c2..c537ac92a 100644 --- a/docs/patterns/openapi.mdx +++ b/docs/patterns/openapi.mdx @@ -1,174 +1,153 @@ --- title: OpenAPI Integration sidebarTitle: OpenAPI -description: Automatically create FastMCP servers from existing OpenAPI specifications. +description: Generate MCP servers from OpenAPI specs icon: code-branch --- -If you have existing REST APIs documented with the OpenAPI Specification (OAS), FastMCP can automatically generate MCP tools, resources, and resource templates directly from that specification. This provides a quick way to make your existing HTTP APIs accessible to MCP clients and LLMs. +FastMCP can automatically generate an MCP server from an OpenAPI specification. Users only need to provide an OpenAPI specification (3.0 or 3.1) and an API client. -FastMCP supports both OpenAPI 3.0 and 3.1 specifications for maximum compatibility with existing API definitions. +```python +import httpx +from fastmcp import FastMCP -## The Goal: API -> MCP Server +# Create a client for your API +api_client = httpx.AsyncClient(base_url="https://api.example.com") -The core idea is to map OpenAPI paths and operations (like `GET /users/{id}` or `POST /orders`) to their corresponding MCP components: +# Load your OpenAPI spec +spec = {...} -- `GET` requests often map to MCP **Resources** (for fetching single items) or **Resource Templates** (if the path has parameters). -- `POST`, `PUT`, `PATCH`, `DELETE` requests typically map to MCP **Tools** (for actions that create or modify data). +# Create an MCP server from your OpenAPI spec +mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client) -FastMCP automates this mapping process. +if __name__ == "__main__": + mcp.run() +``` -## Creating from OpenAPI Spec +## Route Mapping -Use the `FastMCP.from_openapi()` class method. You need: +By default, OpenAPI routes are mapped to MCP components based on these rules: -1. The OpenAPI specification as a Python dictionary. -2. An `httpx.AsyncClient` configured to make requests to the actual API backend. +| OpenAPI Route | Example |MCP Component | Notes | +|- | - | - | - | +| `GET` without path params | `GET /stats` | Resource | Simple resources for fetching data | +| `GET` with path params | `GET /users/{id}` | Resource Template | Path parameters become template parameters | +| `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | Tool | Operations that modify data | - -```python server.py +Internally, FastMCP uses a priority-ordered set of `RouteMap` objects to determine the component type. Route maps indicate that a specific HTTP method (or methods) and path pattern should be treated as a specific component type. This is the default set of route maps: + +```python +# Simplified version of the actual mapping rules +DEFAULT_ROUTE_MAPPINGS = [ + # GET with path parameters -> ResourceTemplate + RouteMap(methods=["GET"], pattern=r".*\{.*\}.*", + route_type=RouteType.RESOURCE_TEMPLATE), + + # GET without path parameters -> Resource + RouteMap(methods=["GET"], pattern=r".*", + route_type=RouteType.RESOURCE), + + # All other methods -> Tool + RouteMap(methods=["POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"], + pattern=r".*", route_type=RouteType.TOOL), +] +``` + +### Custom Route Maps + +Users can add custom route maps to override the default mapping behavior. User-supplied route maps are always applied first, before the default route maps. + +```python +from fastmcp.server.openapi import RouteMap, RouteType + +# Custom mapping rules +custom_maps = [ + # Force all analytics endpoints to be Tools + RouteMap(methods=["GET"], + pattern=r"^/analytics/.*", + route_type=RouteType.TOOL) +] + +# Apply custom mappings +mcp = await FastMCP.from_openapi( + openapi_spec=spec, + client=api_client, + route_maps=custom_maps +) +``` + +## How It Works + +1. FastMCP parses your OpenAPI spec to extract routes and schemas +2. It applies mapping rules to categorize each route +3. When an MCP client calls a tool or accesses a resource: + - FastMCP constructs an HTTP request based on the OpenAPI definition + - It sends the request through the provided httpx client + - It translates the HTTP response to the appropriate MCP format + +## Complete Example + +```python import asyncio import httpx from fastmcp import FastMCP -# load the OpenAPI specification from the openapi_spec.py file -petstore_spec = PETSTORE_SPEC - -# Client to communicate with the actual Pet Store API backend -# The base_url should match the server URL in the OpenAPI spec -http_client = httpx.AsyncClient(base_url="http://petstore.example.com/api") - -# Create the FastMCP server from the spec -# This is an async class method -async def create_openapi_server(): - mcp_server = await FastMCP.from_openapi( - openapi_spec=petstore_spec, - client=http_client, - name="PetStoreMCP" # Optional name for the MCP server - ) - return mcp_server - -async def run_server(): - server = await create_openapi_server() - print(f"Starting OpenAPI-based server '{server.name}'...") - - # List discovered components - tools = await server.list_tools() - resources = await server.list_resources() - templates = await server.list_resource_templates() - print("Discovered Tools:", [t.name for t in tools]) - print("Discovered Resources:", [r.uri for r in resources]) # Should be empty if no parameterless GETs - print("Discovered Templates:", [t.uriTemplate for t in templates]) - - # Run the server (e.g., via stdio) - # server.run() - -if __name__ == "__main__": - # Example: Create the server and print discovered components - # Requires httpx: uv pip install httpx - asyncio.run(run_server()) - -# Expected Output might include: -# Discovered Tools: ['listPets', 'createPet'] -# Discovered Resources: [] -# Discovered Templates: ['resource://openapi/showPetById/{petId}'] -``` - -```python openapi_spec.py -# Example OpenAPI Specification (simplified Pet Store) -PETSTORE_SPEC = { - "openapi": "3.1.0", - "info": {"title": "Simple Pet Store", "version": "1.0.0"}, - "servers": [{"url": "http://petstore.example.com/api"}], # Base URL for API calls +# Sample OpenAPI spec for a Pet Store API +petstore_spec = { + "openapi": "3.0.0", "paths": { "/pets": { "get": { - "summary": "List all pets", "operationId": "listPets", - "tags": ["pets"], - "parameters": [{ # Query parameter -> Tool argument - "name": "limit", "in": "query", "schema": {"type": "integer"} - }], - "responses": {"200": {"description": "A list of pets."}}, + "summary": "List all pets" }, - "post": { # POST -> Tool - "summary": "Create a pet", + "post": { "operationId": "createPet", - "tags": ["pets"], - "requestBody": { # Request body -> Tool arguments - "required": True, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PetInput"}}} - }, - "responses": {"201": {"description": "Pet created."}}, - }, + "summary": "Create a new pet" + } }, - "/pets/{petId}": { # Path parameter -> Resource Template - "get": { # GET with path param -> Resource Template / FunctionResource - "summary": "Info for a specific pet", - "operationId": "showPetById", - "tags": ["pets"], - "parameters": [{ # Path parameter -> Template function argument - "name": "petId", "in": "path", "required": True, "schema": {"type": "string"} - }], - "responses": {"200": {"description": "Information about the pet."}}, - }, - }, - }, - "components": { - "schemas": { - "PetInput": {"type": "object", "properties": {"name": {"type": "string"}, "tag": {"type": "string"}}}, + "/pets/{petId}": { + "get": { + "operationId": "getPet", + "summary": "Get a pet by ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": True, + "schema": {"type": "string"} + } + ] + } } } } -``` - - -### How it Works Internally - -1. **Parsing**: `from_openapi` parses the spec using utilities that leverage `openapi-pydantic`. It extracts paths, operations, parameters, request bodies, and responses. -2. **Mapping**: It applies mapping rules (see below) to decide whether each OpenAPI operation (`GET /pets`, `POST /pets`, `GET /pets/{petId}`) becomes an MCP `Tool`, `Resource`, or `ResourceTemplate`. -3. **Component Creation**: It creates specialized internal components (`OpenAPITool`, `OpenAPIResource`, `OpenAPIResourceTemplate`). -4. **HTTP Execution**: When an MCP client calls a tool or reads a resource from this server: - * The corresponding OpenAPI component constructs an HTTP request based on the OpenAPI definition and the arguments provided by the MCP client. - * It uses the provided `httpx.AsyncClient` to send the request to the backend API. - * It processes the HTTP response and returns it to the MCP client in the appropriate MCP format. -5. **Schema Generation**: The schemas for MCP tools are derived by combining OpenAPI parameters (path, query, header) and request body schemas. Resource template function arguments are derived from path parameters. -6. **Descriptions**: Tool/Resource descriptions are enhanced with information from OpenAPI responses to give the LLM more context about potential outcomes. - -### Default Mapping Rules - -FastMCP uses the following default rules to map OpenAPI operations: - -- `GET` operation with path parameters (e.g., `/users/{id}`) -> **`ResourceTemplate`** -- `GET` operation without path parameters (e.g., `/users`) -> **`Resource`** -- `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `HEAD` -> **`Tool`** - -### Customize Route Mapping - -You can customize the mapping rules by providing a list of `RouteMap` objects directly to `FastMCP.from_openapi()` using the `route_maps` parameter: - -```python -from fastmcp.server.openapi import RouteMap, RouteType -from fastmcp import FastMCP - -# Custom mapping: Treat GET /admin/stats as a Tool, not a Resource -custom_maps = [ - RouteMap(methods=["GET"], pattern=r"^/admin/stats$", route_type=RouteType.TOOL) -] - -async def create_server_with_custom_mapping(): - mcp_server = await FastMCP.from_openapi( +async def main(): + # Client for the Pet Store API + client = httpx.AsyncClient(base_url="https://petstore.example.com/api") + + # Create the MCP server + mcp = await FastMCP.from_openapi( openapi_spec=petstore_spec, - client=http_client, - name="PetStoreMCP", - route_maps=custom_maps # Pass custom mapping rules + client=client, + name="PetStore" ) - return mcp_server + + # List what components were created + tools = await mcp.list_tools() + resources = await mcp.list_resources() + templates = await mcp.list_resource_templates() + + print(f"Tools: {len(tools)}") # Should include createPet + print(f"Resources: {len(resources)}") # Should include listPets + print(f"Templates: {len(templates)}") # Should include getPet + + # Start the MCP server + mcp.run() + +if __name__ == "__main__": + asyncio.run(main()) ``` -Each `RouteMap` maps one or more HTTP methods and a regular expression pattern for the route path to an MCP `RouteType`. Route maps are processed in order, and the first match wins. - -All parameters passed to `FastMCP.from_openapi()` will be forwarded to the underlying `FastMCPOpenAPI` constructor, so you can customize any aspect of the OpenAPI integration directly through this method call. - diff --git a/pyproject.toml b/pyproject.toml index a2a2297ed..0943452cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,13 @@ description = "The fast, Pythonic way to build MCP servers." authors = [{ name = "Jeremiah Lowin" }] dependencies = [ "dotenv>=0.9.9", + "exceptiongroup>=1.2.2", + "httpx>=0.28.1", "mcp>=1.6.0,<2.0.0", + "openapi-pydantic>=0.5.1", "rich>=13.9.4", "typer>=0.15.2", "websockets>=15.0.1", - "fastapi>=0.115.12", - "openapi-pydantic>=0.5.1", - "exceptiongroup>=1.2.2", ] requires-python = ">=3.10" readme = "README.md" @@ -38,6 +38,11 @@ classifiers = [ [dependency-groups] dev = [ + "copychat>=0.5.2", + "dirty-equals>=0.9.0", + "fastapi>=0.115.12", + "ipython>=8.12.3", + "pdbpp>=0.10.3", "pre-commit", "pyright>=1.1.389", "pytest>=8.3.3", @@ -45,10 +50,6 @@ dev = [ "pytest-flakefinder", "pytest-xdist>=3.6.1", "ruff", - "copychat>=0.5.2", - "ipython>=8.12.3", - "pdbpp>=0.10.3", - "dirty-equals>=0.9.0", ] [project.scripts] diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index facd5ff18..8fd15d755 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -13,7 +13,6 @@ 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 @@ -846,11 +845,12 @@ class FastMCP(Generic[LifespanResultT]): @classmethod def from_fastapi( - cls, app: FastAPI, name: str | None = None, **settings: Any + cls, app: "Any", name: str | None = None, **settings: Any ) -> "FastMCPOpenAPI": """ Create a FastMCP server from a FastAPI application. """ + from .openapi import FastMCPOpenAPI client = httpx.AsyncClient( diff --git a/uv.lock b/uv.lock index 6fb683270..a12924537 100644 --- a/uv.lock +++ b/uv.lock @@ -254,12 +254,12 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.1.3.dev31+8d922d9" +version = "2.1.3.dev42+be2ccc6" source = { editable = "." } dependencies = [ { name = "dotenv" }, { name = "exceptiongroup" }, - { name = "fastapi" }, + { name = "httpx" }, { name = "mcp" }, { name = "openapi-pydantic" }, { name = "rich" }, @@ -271,6 +271,7 @@ dependencies = [ dev = [ { name = "copychat" }, { name = "dirty-equals" }, + { name = "fastapi" }, { name = "ipython" }, { name = "pdbpp" }, { name = "pre-commit" }, @@ -286,7 +287,7 @@ dev = [ requires-dist = [ { name = "dotenv", specifier = ">=0.9.9" }, { name = "exceptiongroup", specifier = ">=1.2.2" }, - { name = "fastapi", specifier = ">=0.115.12" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "mcp", specifier = ">=1.6.0,<2.0.0" }, { name = "openapi-pydantic", specifier = ">=0.5.1" }, { name = "rich", specifier = ">=13.9.4" }, @@ -298,6 +299,7 @@ requires-dist = [ dev = [ { name = "copychat", specifier = ">=0.5.2" }, { name = "dirty-equals", specifier = ">=0.9.0" }, + { name = "fastapi", specifier = ">=0.115.12" }, { name = "ipython", specifier = ">=8.12.3" }, { name = "pdbpp", specifier = ">=0.10.3" }, { name = "pre-commit" },