diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 2edb6ef60..a92024d6f 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -48,7 +48,13 @@ jobs:
run: uv sync --upgrade
- name: Run tests (excluding integration and client_process)
- run: uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process" ${{ matrix.os == 'windows-latest' && '' || '--numprocesses auto --maxprocesses 4 --dist worksteal' }}
+ run: |
+ if [ "${{ matrix.os }}" = "windows-latest" ]; then
+ uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process"
+ else
+ uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process" --numprocesses auto --maxprocesses 4 --dist worksteal
+ fi
+ shell: bash
- name: Run client process tests separately
run: uv run pytest --inline-snapshot=disable tests -m "client_process" -x
diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx
new file mode 100644
index 000000000..aca7b79f1
--- /dev/null
+++ b/docs/deployment/http.mdx
@@ -0,0 +1,496 @@
+---
+title: HTTP Deployment
+sidebarTitle: HTTP Deployment
+description: Deploy your FastMCP server over HTTP for remote access
+icon: server
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+STDIO transport is perfect for local development and desktop applications. But to unlock the full potential of MCP—centralized services, multi-client access, and network availability—you need remote HTTP deployment.
+
+
+This guide walks you through deploying your FastMCP server as a remote MCP service that's accessible via a URL. Once deployed, your MCP server will be available over the network, allowing multiple clients to connect simultaneously and enabling integration with cloud-based LLM applications. This guide focuses specifically on remote MCP deployment, not local STDIO servers.
+
+## Choosing Your Approach
+
+FastMCP provides two ways to deploy your server as an HTTP service. Understanding the trade-offs helps you choose the right approach for your needs.
+
+The **direct HTTP server** approach is simpler and perfect for getting started quickly. You modify your server's `run()` method to use HTTP transport, and FastMCP handles all the web server configuration. This approach works well for standalone deployments where you want your MCP server to be the only service running on a port.
+
+The **ASGI application** approach gives you more control and flexibility. Instead of running the server directly, you create an ASGI application that can be served by Uvicorn. This approach is better when you need advanced server features like multiple workers, custom middleware, or when you're integrating with existing web applications.
+
+### Direct HTTP Server
+
+The simplest way to get your MCP server online is to use the built-in `run()` method with HTTP transport. This approach handles all the server configuration for you and is ideal when you want a standalone MCP server without additional complexity.
+
+```python server.py
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+def process_data(input: str) -> str:
+ """Process data on the server"""
+ return f"Processed: {input}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+Run your server with a simple Python command:
+```bash
+python server.py
+```
+
+Your server is now accessible at `http://localhost:8000/mcp/` (or use your server's actual IP address for remote access).
+
+This approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.
+
+### ASGI Application
+
+For production deployments, you'll often want more control over how your server runs. FastMCP can create a standard ASGI application that works with any ASGI server like Uvicorn, Gunicorn, or Hypercorn. This approach is particularly useful when you need to configure advanced server options, run multiple workers, or integrate with existing infrastructure.
+
+```python app.py
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+def process_data(input: str) -> str:
+ """Process data on the server"""
+ return f"Processed: {input}"
+
+# Create ASGI application
+app = mcp.http_app()
+```
+
+Run with any ASGI server - here's an example with Uvicorn:
+```bash
+uvicorn app:app --host 0.0.0.0 --port 8000
+```
+
+Your server is accessible at the same URL: `http://localhost:8000/mcp/` (or use your server's actual IP address for remote access).
+
+The ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application.
+
+## Configuring Your Server
+
+### Custom Path
+
+By default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
+
+```python
+# Option 1: With mcp.run()
+mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp/")
+
+# Option 2: With ASGI app
+app = mcp.http_app(path="/api/mcp/")
+```
+
+Now your server is accessible at `http://localhost:8000/api/mcp/`.
+
+### Authentication
+
+
+Authentication is **highly recommended** for remote MCP servers. Some LLM clients require authentication for remote servers and will refuse to connect without it.
+
+
+FastMCP supports multiple authentication methods to secure your remote server. See the [Authentication Overview](/servers/auth/authentication) for complete configuration options including Bearer tokens, JWT, and OAuth.
+
+If you're mounting an authenticated server under a path prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) below for important routing considerations.
+
+### Health Checks
+
+Health check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.
+
+```python
+from starlette.responses import JSONResponse
+
+@mcp.custom_route("/health", methods=["GET"])
+async def health_check(request):
+ return JSONResponse({"status": "healthy", "service": "mcp-server"})
+```
+
+This health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.
+
+### Custom Middleware
+
+
+
+
+Add custom Starlette middleware to your FastMCP ASGI apps:
+
+```python
+from fastmcp import FastMCP
+from starlette.middleware import Middleware
+from starlette.middleware.cors import CORSMiddleware
+
+# Create your FastMCP server
+mcp = FastMCP("MyServer")
+
+# Define middleware
+middleware = [
+ Middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+]
+
+# Create ASGI app with middleware
+http_app = mcp.http_app(middleware=middleware)
+```
+
+## Integration with Web Frameworks
+
+If you already have a web application running, you can add MCP capabilities by mounting a FastMCP server as a sub-application. This allows you to expose MCP tools alongside your existing API endpoints, sharing the same domain and infrastructure. The MCP server becomes just another route in your application, making it easy to manage and deploy.
+
+### Mounting in Starlette
+
+Mount your FastMCP server in a Starlette application:
+
+```python
+from fastmcp import FastMCP
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+# Create your FastMCP server
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def analyze(data: str) -> dict:
+ return {"result": f"Analyzed: {data}"}
+
+# Create the ASGI app
+mcp_app = mcp.http_app(path='/mcp')
+
+# Create a Starlette app and mount the MCP server
+app = Starlette(
+ routes=[
+ Mount("/mcp-server", app=mcp_app),
+ # Add other routes as needed
+ ],
+ lifespan=mcp_app.lifespan,
+)
+```
+
+The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
+
+
+For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
+
+
+#### Nested Mounts
+
+You can create complex routing structures by nesting mounts:
+
+```python
+from fastmcp import FastMCP
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+# Create your FastMCP server
+mcp = FastMCP("MyServer")
+
+# Create the ASGI app
+mcp_app = mcp.http_app(path='/mcp')
+
+# Create nested application structure
+inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
+app = Starlette(
+ routes=[Mount("/outer", app=inner_app)],
+ lifespan=mcp_app.lifespan,
+)
+```
+
+In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.
+
+### FastAPI Integration
+
+For FastAPI-specific integration patterns including both mounting MCP servers into FastAPI apps and generating MCP servers from FastAPI apps, see the [FastAPI Integration guide](/integrations/fastapi).
+
+Here's a quick example showing how to add MCP to an existing FastAPI application:
+
+```python
+from fastapi import FastAPI
+from fastmcp import FastMCP
+
+# Your existing API
+api = FastAPI()
+
+@api.get("/api/status")
+def status():
+ return {"status": "ok"}
+
+# Create your MCP server
+mcp = FastMCP("API Tools")
+
+@mcp.tool
+def query_database(query: str) -> dict:
+ """Run a database query"""
+ return {"result": "data"}
+
+# Mount MCP at /mcp
+api.mount("/mcp", mcp.http_app())
+
+# Run with: uvicorn app:api --host 0.0.0.0 --port 8000
+```
+
+Your existing API remains at `http://localhost:8000/api/` while MCP is available at `http://localhost:8000/mcp/`.
+
+## Mounting Authenticated Servers
+
+
+
+
+This section only applies if you're **mounting an OAuth-protected FastMCP server under a path prefix** (like `/api`) inside another application using `Mount()`.
+
+If you're deploying your FastMCP server at root level without any `Mount()` prefix, the well-known routes are automatically included in `mcp.http_app()` and you don't need to do anything special.
+
+
+OAuth specifications (RFC 8414 and RFC 9728) require discovery metadata to be accessible at well-known paths under the root level of your domain. When you mount an OAuth-protected FastMCP server under a path prefix like `/api`, this creates a routing challenge: your operational OAuth endpoints move under the prefix, but discovery endpoints must remain at the root.
+
+
+**Common Mistakes to Avoid:**
+
+1. **Forgetting to mount `.well-known` routes at root** - FastMCP cannot do this automatically when your server is mounted under a path prefix. You must explicitly mount well-known routes at the root level.
+
+2. **Including mount prefix in both base_url AND mcp_path** - The mount prefix (like `/api`) should only be in `base_url`, not in `mcp_path`. Otherwise you'll get double paths.
+
+ ✅ **Correct:**
+ ```python
+ base_url = "http://localhost:8000/api"
+ mcp_path = "/mcp"
+ # Result: /api/mcp
+ ```
+
+ ❌ **Wrong:**
+ ```python
+ base_url = "http://localhost:8000/api"
+ mcp_path = "/api/mcp"
+ # Result: /api/api/mcp (double prefix!)
+ ```
+
+3. **Not setting issuer_url when mounting** - Without `issuer_url` set to root level, OAuth discovery will attempt path-scoped discovery first (which will 404), adding unnecessary error logs.
+
+Follow the configuration instructions below to set up mounting correctly.
+
+
+### Route Types
+
+OAuth-protected MCP servers expose two categories of routes:
+
+**Operational routes** handle the OAuth flow and MCP protocol:
+- `/authorize` - OAuth authorization endpoint
+- `/token` - Token exchange endpoint
+- `/auth/callback` - OAuth callback handler
+- `/mcp` - MCP protocol endpoint
+
+**Discovery routes** provide metadata for OAuth clients:
+- `/.well-known/oauth-authorization-server` - Authorization server metadata
+- `/.well-known/oauth-protected-resource/*` - Protected resource metadata
+
+When you mount your MCP app under a prefix, operational routes move with it, but discovery routes must stay at root level for RFC compliance.
+
+### Configuration Parameters
+
+Three parameters control where routes are located and how they combine:
+
+**`base_url`** tells clients where to find operational endpoints. This includes any Starlette `Mount()` path prefix (e.g., `/api`):
+
+```python
+base_url="http://localhost:8000/api" # Includes mount prefix
+```
+
+**`mcp_path`** is the internal FastMCP endpoint path, which gets appended to `base_url`:
+
+```python
+mcp_path="/mcp" # Internal MCP path, NOT the mount prefix
+```
+
+**`issuer_url`** tells clients where to find discovery metadata. This should point to the root level of your server where well-known routes are mounted:
+
+```python
+issuer_url="http://localhost:8000" # Root level, no prefix
+```
+
+**Key Invariant:** `base_url + mcp_path = actual externally-accessible MCP URL`
+
+Example:
+- `base_url`: `http://localhost:8000/api` (mount prefix `/api`)
+- `mcp_path`: `/mcp` (internal path)
+- Result: `http://localhost:8000/api/mcp` (final MCP endpoint)
+
+Note that the mount prefix (`/api` from `Mount("/api", ...)`) goes in `base_url`, while `mcp_path` is just the internal MCP route. Don't include the mount prefix in both places or you'll get `/api/api/mcp`.
+
+### Mounting Strategy
+
+When mounting an OAuth-protected server under a path prefix, declare your URLs upfront to make the relationships clear:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+# Define the routing structure
+ROOT_URL = "http://localhost:8000"
+MOUNT_PREFIX = "/api"
+MCP_PATH = "/mcp"
+```
+
+Create the auth provider with both `issuer_url` and `base_url`:
+
+```python
+auth = GitHubProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ issuer_url=ROOT_URL, # Discovery metadata at root
+ base_url=f"{ROOT_URL}{MOUNT_PREFIX}", # Operational endpoints under prefix
+)
+```
+
+Create the MCP app, which generates operational routes at the specified path:
+
+```python
+mcp = FastMCP("Protected Server", auth=auth)
+mcp_app = mcp.http_app(path=MCP_PATH)
+```
+
+Retrieve the discovery routes from the auth provider. The `mcp_path` argument should match the path used when creating the MCP app:
+
+```python
+well_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)
+```
+
+Finally, mount everything in the Starlette app with discovery routes at root and the MCP app under the prefix:
+
+```python
+app = Starlette(
+ routes=[
+ *well_known_routes, # Discovery routes at root level
+ Mount(MOUNT_PREFIX, app=mcp_app), # Operational routes under prefix
+ ],
+ lifespan=mcp_app.lifespan,
+)
+```
+
+This configuration produces the following URL structure:
+
+- MCP endpoint: `http://localhost:8000/api/mcp`
+- OAuth authorization: `http://localhost:8000/api/authorize`
+- OAuth callback: `http://localhost:8000/api/auth/callback`
+- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server`
+- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp`
+
+### Complete Example
+
+Here's a complete working example showing all the pieces together:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+from starlette.applications import Starlette
+from starlette.routing import Mount
+import uvicorn
+
+# Define routing structure
+ROOT_URL = "http://localhost:8000"
+MOUNT_PREFIX = "/api"
+MCP_PATH = "/mcp"
+
+# Create OAuth provider
+auth = GitHubProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ issuer_url=ROOT_URL,
+ base_url=f"{ROOT_URL}{MOUNT_PREFIX}",
+)
+
+# Create MCP server
+mcp = FastMCP("Protected Server", auth=auth)
+
+@mcp.tool
+def analyze(data: str) -> dict:
+ return {"result": f"Analyzed: {data}"}
+
+# Create MCP app
+mcp_app = mcp.http_app(path=MCP_PATH)
+
+# Get discovery routes for root level
+well_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)
+
+# Assemble the application
+app = Starlette(
+ routes=[
+ *well_known_routes,
+ Mount(MOUNT_PREFIX, app=mcp_app),
+ ],
+ lifespan=mcp_app.lifespan,
+)
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+```
+
+For more details on OAuth authentication, see the [Authentication guide](/servers/auth).
+
+## Production Deployment
+
+### Running with Uvicorn
+
+When deploying to production, you'll want to optimize your server for performance and reliability. Uvicorn provides several options to improve your server's capabilities:
+
+```bash
+# Run with basic configuration
+uvicorn app:app --host 0.0.0.0 --port 8000
+
+# Run with multiple workers for production
+uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
+```
+
+### Environment Variables
+
+Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
+
+Here's an example using bearer token authentication (though OAuth is recommended for production):
+
+```python
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth import BearerTokenAuth
+
+# Read configuration from environment
+auth_token = os.environ.get("MCP_AUTH_TOKEN")
+if auth_token:
+ auth = BearerTokenAuth(token=auth_token)
+ mcp = FastMCP("Production Server", auth=auth)
+else:
+ mcp = FastMCP("Production Server")
+
+app = mcp.http_app()
+```
+
+Deploy with your secrets safely stored in environment variables:
+```bash
+MCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000
+```
+
+## Testing Your Deployment
+
+Once your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/development/tests) guide.
+
+## Hosting Your Server
+
+This guide has shown you how to create an HTTP-accessible MCP server, but you'll still need a hosting provider to make it available on the internet. Your FastMCP server can run anywhere that supports Python web applications:
+
+- **Cloud VMs** (AWS EC2, Google Compute Engine, Azure VMs)
+- **Container platforms** (Cloud Run, Container Instances, ECS)
+- **Platform-as-a-Service** (Railway, Render, Vercel)
+- **Edge platforms** (Cloudflare Workers)
+- **Kubernetes clusters** (self-managed or managed)
+
+The key requirements are Python 3.10+ support and the ability to expose an HTTP port. Most providers will require you to package your server (requirements.txt, Dockerfile, etc.) according to their deployment format. For managed, zero-configuration deployment, see [FastMCP Cloud](/deployment/fastmcp-cloud).
\ No newline at end of file
diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx
index 28f1217a7..5aa0d7819 100644
--- a/docs/deployment/running-server.mdx
+++ b/docs/deployment/running-server.mdx
@@ -1,11 +1,11 @@
---
title: Running Your Server
-sidebarTitle: Running
+sidebarTitle: Running Your Server
description: Learn how to run your FastMCP server locally for development and testing
icon: circle-play
---
-FastMCP servers can be run in different ways depending on your needs. This guide focuses on running servers locally for development and testing. For production deployment to a URL, see the [Self-Hosted Deployment](/deployment/self-hosted) guide.
+FastMCP servers can be run in different ways depending on your needs. This guide focuses on running servers locally for development and testing. For production deployment to a URL, see the [HTTP Deployment](/deployment/http) guide.
## The `run()` Method
@@ -89,7 +89,7 @@ Your server is now accessible at `http://localhost:8000/mcp/`. This URL is the M
- Integration with web infrastructure
- Remote deployment capabilities
-For production HTTP deployment with authentication and advanced configuration, see the [Self-Hosted Deployment](/deployment/self-hosted) guide.
+For production HTTP deployment with authentication and advanced configuration, see the [HTTP Deployment](/deployment/http) guide.
### SSE Transport (Legacy)
@@ -212,7 +212,7 @@ if __name__ == "__main__":
mcp.run(transport="http") # Health check at http://localhost:8000/health
```
-Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/self-hosted#integration-with-web-frameworks).
+Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
## Alternative Initialization Patterns
@@ -255,4 +255,4 @@ def create_app():
app = create_app() # Uvicorn will use this
```
-See the [Self-Hosted Deployment](/deployment/self-hosted) guide for more ASGI deployment patterns.
\ No newline at end of file
+See the [HTTP Deployment](/deployment/http) guide for more ASGI deployment patterns.
\ No newline at end of file
diff --git a/docs/deployment/self-hosted.mdx b/docs/deployment/self-hosted.mdx
deleted file mode 100644
index 201e00bcb..000000000
--- a/docs/deployment/self-hosted.mdx
+++ /dev/null
@@ -1,209 +0,0 @@
----
-title: Self-Hosted Remote MCP
-sidebarTitle: Self-Hosted
-description: Deploy your FastMCP server as a remote MCP service accessible via URL
-icon: server
----
-
-
-STDIO transport is perfect for local development and desktop applications. But to unlock the full potential of MCP—centralized services, multi-client access, and network availability—you need remote HTTP deployment.
-
-
-This guide walks you through deploying your FastMCP server as a remote MCP service that's accessible via a URL. Once deployed, your MCP server will be available over the network, allowing multiple clients to connect simultaneously and enabling integration with cloud-based LLM applications. This guide focuses specifically on remote MCP deployment, not local STDIO servers.
-
-## Choosing Your Approach
-
-FastMCP provides two ways to deploy your server as an HTTP service. Understanding the trade-offs helps you choose the right approach for your needs.
-
-The **direct HTTP server** approach is simpler and perfect for getting started quickly. You modify your server's `run()` method to use HTTP transport, and FastMCP handles all the web server configuration. This approach works well for standalone deployments where you want your MCP server to be the only service running on a port.
-
-The **ASGI application** approach gives you more control and flexibility. Instead of running the server directly, you create an ASGI application that can be served by production-grade servers like Uvicorn or Gunicorn. This approach is better when you need advanced server features like multiple workers, custom middleware, or when you're integrating with existing web applications.
-
-### Direct HTTP Server
-
-The simplest way to get your MCP server online is to use the built-in `run()` method with HTTP transport. This approach handles all the server configuration for you and is ideal when you want a standalone MCP server without additional complexity.
-
-```python server.py
-from fastmcp import FastMCP
-
-mcp = FastMCP("My Server")
-
-@mcp.tool
-def process_data(input: str) -> str:
- """Process data on the server"""
- return f"Processed: {input}"
-
-if __name__ == "__main__":
- mcp.run(transport="http", host="0.0.0.0", port=8000)
-```
-
-Run your server with a simple Python command:
-```bash
-python server.py
-```
-
-Your server is now accessible at `http://localhost:8000/mcp/` (or use your server's actual IP address for remote access).
-
-This approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.
-
-### ASGI Application
-
-For production deployments, you'll often want more control over how your server runs. FastMCP can create a standard ASGI application that works with any ASGI server like Uvicorn, Gunicorn, or Hypercorn. This approach is particularly useful when you need to configure advanced server options, run multiple workers, or integrate with existing infrastructure.
-
-```python app.py
-from fastmcp import FastMCP
-
-mcp = FastMCP("My Server")
-
-@mcp.tool
-def process_data(input: str) -> str:
- """Process data on the server"""
- return f"Processed: {input}"
-
-# Create ASGI application
-app = mcp.http_app()
-```
-
-Run with any ASGI server - here's an example with Uvicorn:
-```bash
-uvicorn app:app --host 0.0.0.0 --port 8000
-```
-
-Your server is accessible at the same URL: `http://localhost:8000/mcp/` (or use your server's actual IP address for remote access).
-
-The ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application. This flexibility makes it the preferred choice for serious deployments.
-
-## Configuring Your Server
-
-### Custom Path
-
-By default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
-
-```python
-# Option 1: With mcp.run()
-mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp/")
-
-# Option 2: With ASGI app
-app = mcp.http_app(path="/api/mcp/")
-```
-
-Now your server is accessible at `http://localhost:8000/api/mcp/`.
-
-### Authentication
-
-
-Authentication is **highly recommended** for remote MCP servers. Some LLM clients require authentication for remote servers and will refuse to connect without it.
-
-
-FastMCP supports multiple authentication methods to secure your remote server. See the [Authentication Overview](/servers/auth/authentication) for complete configuration options including Bearer tokens, JWT, and OAuth.
-
-### Health Checks
-
-Health check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.
-
-```python
-from starlette.responses import JSONResponse
-
-@mcp.custom_route("/health", methods=["GET"])
-async def health_check(request):
- return JSONResponse({"status": "healthy", "service": "mcp-server"})
-```
-
-This health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.
-
-## Integration with Web Frameworks
-
-If you already have a web application running, you can add MCP capabilities by mounting a FastMCP server as a sub-application. This allows you to expose MCP tools alongside your existing API endpoints, sharing the same domain and infrastructure. The MCP server becomes just another route in your application, making it easy to manage and deploy.
-
-For detailed integration guides, see:
-- [FastAPI Integration](/integrations/fastapi)
-- [ASGI / Starlette Integration](/integrations/asgi)
-
-Here's a quick example showing how to add MCP to an existing FastAPI application:
-
-```python
-from fastapi import FastAPI
-from fastmcp import FastMCP
-
-# Your existing API
-api = FastAPI()
-
-@api.get("/api/status")
-def status():
- return {"status": "ok"}
-
-# Create your MCP server
-mcp = FastMCP("API Tools")
-
-@mcp.tool
-def query_database(query: str) -> dict:
- """Run a database query"""
- return {"result": "data"}
-
-# Mount MCP at /mcp
-api.mount("/mcp", mcp.http_app())
-
-# Run with: uvicorn app:api --host 0.0.0.0 --port 8000
-```
-
-Your existing API remains at `http://localhost:8000/api/` while MCP is available at `http://localhost:8000/mcp/`.
-
-## Production Deployment
-
-### Running with Uvicorn
-
-When deploying to production, you'll want to optimize your server for performance and reliability. Uvicorn provides several options to improve your server's capabilities, including running multiple worker processes to handle concurrent requests and enabling enhanced logging for monitoring.
-
-```bash
-# Install uvicorn with standard extras for better performance
-pip install 'uvicorn[standard]'
-
-# Run with multiple workers for better concurrency
-uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
-
-# Enable detailed logging for monitoring
-uvicorn app:app --host 0.0.0.0 --port 8000 --log-level info
-```
-
-### Environment Variables
-
-Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
-
-Here's an example using bearer token authentication (though OAuth is recommended for production):
-
-```python
-import os
-from fastmcp import FastMCP
-from fastmcp.server.auth import BearerTokenAuth
-
-# Read configuration from environment
-auth_token = os.environ.get("MCP_AUTH_TOKEN")
-if auth_token:
- auth = BearerTokenAuth(token=auth_token)
- mcp = FastMCP("Production Server", auth=auth)
-else:
- mcp = FastMCP("Production Server")
-
-app = mcp.http_app()
-```
-
-Deploy with your secrets safely stored in environment variables:
-```bash
-MCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000
-```
-
-## Testing Your Deployment
-
-Once your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/development/tests) guide.
-
-## Hosting Your Server
-
-This guide has shown you how to create an HTTP-accessible MCP server, but you'll still need a hosting provider to make it available on the internet. Your FastMCP server can run anywhere that supports Python web applications:
-
-- **Cloud VMs** (AWS EC2, Google Compute Engine, Azure VMs)
-- **Container platforms** (Cloud Run, Container Instances, ECS)
-- **Platform-as-a-Service** (Railway, Render, Vercel)
-- **Edge platforms** (Cloudflare Workers)
-- **Kubernetes clusters** (self-managed or managed)
-
-The key requirements are Python 3.10+ support and the ability to expose an HTTP port. Most providers will require you to package your server (requirements.txt, Dockerfile, etc.) according to their deployment format. For managed, zero-configuration deployment, see [FastMCP Cloud](/deployment/fastmcp-cloud).
\ No newline at end of file
diff --git a/docs/docs.json b/docs/docs.json
index 625faac80..b6a1e0fbd 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -20,10 +20,7 @@
"primary": "#2d00f7"
},
"contextual": {
- "options": [
- "copy",
- "view"
- ]
+ "options": ["copy", "view"]
},
"description": "The fast, Pythonic way to build MCP servers and clients.",
"errors": {
@@ -134,9 +131,9 @@
"icon": "rocket",
"pages": [
"deployment/running-server",
- "deployment/server-configuration",
- "deployment/self-hosted",
- "deployment/fastmcp-cloud"
+ "deployment/http",
+ "deployment/fastmcp-cloud",
+ "deployment/server-configuration"
]
}
]
@@ -147,10 +144,7 @@
{
"group": "Essentials",
"icon": "cube",
- "pages": [
- "clients/client",
- "clients/transports"
- ]
+ "pages": ["clients/client", "clients/transports"]
},
{
"group": "Core Operations",
@@ -176,10 +170,7 @@
{
"group": "Authentication",
"icon": "user-shield",
- "pages": [
- "clients/auth/oauth",
- "clients/auth/bearer"
- ]
+ "pages": ["clients/auth/oauth", "clients/auth/bearer"]
}
]
},
@@ -231,13 +222,9 @@
]
},
{
- "group": "Web Frameworks",
+ "group": "API Integration",
"icon": "globe",
- "pages": [
- "integrations/asgi",
- "integrations/fastapi",
- "integrations/openapi"
- ]
+ "pages": ["integrations/fastapi", "integrations/openapi"]
}
]
},
@@ -374,6 +361,7 @@
"group": "middleware",
"pages": [
"python-sdk/fastmcp-server-middleware-__init__",
+ "python-sdk/fastmcp-server-middleware-caching",
"python-sdk/fastmcp-server-middleware-error_handling",
"python-sdk/fastmcp-server-middleware-logging",
"python-sdk/fastmcp-server-middleware-middleware",
diff --git a/docs/integrations/asgi.mdx b/docs/integrations/asgi.mdx
deleted file mode 100644
index 66e7e4055..000000000
--- a/docs/integrations/asgi.mdx
+++ /dev/null
@@ -1,213 +0,0 @@
----
-title: ASGI / Starlette 🤝 FastMCP
-sidebarTitle: ASGI / Starlette
-description: Integrate FastMCP servers into ASGI applications
-icon: server
----
-
-import { VersionBadge } from '/snippets/version-badge.mdx'
-
-
-
-FastMCP servers can be integrated into existing ASGI applications, allowing you to add MCP functionality to your web applications. This is useful for:
-
-- Adding MCP functionality to an existing website or API
-- Mounting MCP servers under specific URL paths
-- Combining multiple services in a single application
-- Leveraging existing authentication and middleware
-
-## Basic Usage
-
-To integrate a FastMCP server into an ASGI application, use the `http_app()` method to obtain a Starlette application instance:
-
-
-The `http_app()` method is new in FastMCP 2.3.2. In older versions, use `sse_app()` for SSE transport or `streamable_http_app()` for Streamable HTTP transport.
-
-
-```python
-from fastmcp import FastMCP
-
-mcp = FastMCP("MyServer")
-
-@mcp.tool
-def hello(name: str) -> str:
- return f"Hello, {name}!"
-
-# Get a Starlette app instance for Streamable HTTP transport (recommended)
-http_app = mcp.http_app()
-
-# For legacy SSE transport (deprecated)
-sse_app = mcp.http_app(transport="sse")
-```
-
-The returned Starlette application can be integrated with other ASGI-compatible web frameworks. The MCP server's endpoint is mounted at `/mcp/` for Streamable HTTP transport and `/sse/` for SSE transport.
-
-### Configuration Options
-
-You can customize the endpoint path and access the FastMCP server instance:
-
-```python
-# Custom endpoint path
-http_app = mcp.http_app(path="/custom-mcp-path")
-
-# Access the FastMCP server from middleware/routes
-# The server is available at: request.app.state.fastmcp_server
-```
-
-### Adding Custom Routes
-
-You can add custom web routes directly to your FastMCP server using the `@custom_route` decorator:
-
-```python
-from fastmcp import FastMCP
-from starlette.requests import Request
-from starlette.responses import JSONResponse
-
-mcp = FastMCP("MyServer")
-
-@mcp.custom_route("/api/status", methods=["GET"])
-async def get_status(request: Request):
- return JSONResponse({"server": "running"})
-
-http_app = mcp.http_app()
-```
-
-#### Health Check Endpoints
-
-Health checks are commonly needed for monitoring and load balancing:
-
-```python
-from fastmcp import FastMCP
-from starlette.requests import Request
-from starlette.responses import JSONResponse
-
-mcp = FastMCP("MyServer")
-
-@mcp.custom_route("/health", methods=["GET"])
-async def health_check(request: Request):
- return JSONResponse({"status": "healthy"})
-
-http_app = mcp.http_app()
-```
-
-The health endpoint will be available at `/health` alongside your MCP endpoint at `/mcp/`.
-
-## Starlette Integration
-
-Mount your FastMCP server in another Starlette application:
-
-```python
-from fastmcp import FastMCP
-from starlette.applications import Starlette
-from starlette.routing import Mount
-
-# Create your FastMCP server
-mcp = FastMCP("MyServer")
-
-@mcp.tool
-def analyze(data: str) -> dict:
- return {"result": f"Analyzed: {data}"}
-
-# Create the ASGI app
-mcp_app = mcp.http_app(path='/mcp')
-
-# Create a Starlette app and mount the MCP server
-app = Starlette(
- routes=[
- Mount("/mcp-server", app=mcp_app),
- # Add other routes as needed
- ],
- lifespan=mcp_app.lifespan,
-)
-```
-
-The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
-
-
-For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
-
-
-### Nested Mounts
-
-You can create complex routing structures by nesting mounts:
-
-```python
-from fastmcp import FastMCP
-from starlette.applications import Starlette
-from starlette.routing import Mount
-
-# Create your FastMCP server
-mcp = FastMCP("MyServer")
-
-# Create the ASGI app
-mcp_app = mcp.http_app(path='/mcp')
-
-# Create nested application structure
-inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
-app = Starlette(
- routes=[Mount("/outer", app=inner_app)],
- lifespan=mcp_app.lifespan,
-)
-```
-
-In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.
-
-## Custom Middleware
-
-
-
-Add custom Starlette middleware to your FastMCP ASGI apps by passing a list of middleware instances:
-
-```python
-from fastmcp import FastMCP
-from starlette.middleware import Middleware
-from starlette.middleware.cors import CORSMiddleware
-
-# Create your FastMCP server
-mcp = FastMCP("MyServer")
-
-# Define custom middleware
-custom_middleware = [
- Middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_methods=["*"],
- allow_headers=["*"],
- )
-]
-
-# Create ASGI app with middleware
-http_app = mcp.http_app(custom_middleware=custom_middleware)
-```
-
-## Running the Server
-
-To run your ASGI application, use an ASGI server like `uvicorn`:
-
-```python
-import uvicorn
-
-if __name__ == "__main__":
- uvicorn.run(app, host="0.0.0.0", port=8000)
-```
-
-Or from the command line:
-
-```bash
-uvicorn path.to.your.app:app --host 0.0.0.0 --port 8000
-```
-
-## Framework-Specific Integration
-
-### FastAPI
-
-For FastAPI-specific integration patterns including both mounting MCP servers into FastAPI apps and generating MCP servers from FastAPI apps, see the [FastAPI Integration guide](/integrations/fastapi).
-
-### Other ASGI Frameworks
-
-The patterns shown here work with any ASGI-compatible framework. The key requirements are:
-
-1. Mount the FastMCP ASGI app at your desired path
-2. Pass the lifespan context to your root application
-3. Configure any necessary middleware or authentication
-
diff --git a/docs/integrations/auth0.mdx b/docs/integrations/auth0.mdx
index 0c8b9ff48..f02594178 100644
--- a/docs/integrations/auth0.mdx
+++ b/docs/integrations/auth0.mdx
@@ -189,7 +189,11 @@ Your Auth0 API Audience
-Public URL of your FastMCP server for OAuth callbacks
+Public URL where OAuth endpoints will be accessible (includes any mount path)
+
+
+
+Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
diff --git a/docs/integrations/aws-cognito.mdx b/docs/integrations/aws-cognito.mdx
index d89f6a1be..748fb4de6 100644
--- a/docs/integrations/aws-cognito.mdx
+++ b/docs/integrations/aws-cognito.mdx
@@ -235,7 +235,11 @@ Your AWS Cognito app client secret
-Public URL of your FastMCP server for OAuth callbacks
+Public URL where OAuth endpoints will be accessible (includes any mount path)
+
+
+
+Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index 525a674b8..5bd84b648 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -243,7 +243,11 @@ This is **REQUIRED**. Find your tenant ID in Azure Portal under Microsoft Entra
-Public URL of your FastMCP server for OAuth callbacks
+Public URL where OAuth endpoints will be accessible (includes any mount path)
+
+
+
+Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
diff --git a/docs/integrations/fastapi.mdx b/docs/integrations/fastapi.mdx
index 53bd9be9c..dcd82b2a3 100644
--- a/docs/integrations/fastapi.mdx
+++ b/docs/integrations/fastapi.mdx
@@ -405,6 +405,8 @@ app = FastAPI()
app.mount("/mcp", mcp.http_app()) # Session manager won't initialize
```
+If you're mounting an authenticated MCP server under a path prefix, see [Mounting Authenticated Servers](/deployment/http#mounting-authenticated-servers) for important OAuth routing considerations.
+
### Combining Lifespans
If your FastAPI app already has a lifespan (for database connections, startup tasks, etc.), you can't simply replace it with the MCP lifespan. Instead, you need to create a new lifespan function that manages both contexts. This ensures that both your app's initialization logic and the MCP server's session manager run properly:
diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx
index 2414dd253..3822e9dc5 100644
--- a/docs/integrations/github.mdx
+++ b/docs/integrations/github.mdx
@@ -165,7 +165,11 @@ Your GitHub OAuth App Client Secret
-Public URL of your FastMCP server for OAuth callbacks
+Public URL where OAuth endpoints will be accessible (includes any mount path)
+
+
+
+Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
diff --git a/docs/integrations/google.mdx b/docs/integrations/google.mdx
index 9e3cf8bab..5cf398776 100644
--- a/docs/integrations/google.mdx
+++ b/docs/integrations/google.mdx
@@ -178,7 +178,11 @@ Your Google OAuth 2.0 Client Secret (e.g., `GOCSPX-abc123...`)
-Public URL of your FastMCP server for OAuth callbacks
+Public URL where OAuth endpoints will be accessible (includes any mount path)
+
+
+
+Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
diff --git a/docs/integrations/workos.mdx b/docs/integrations/workos.mdx
index 98aa6999e..b0f90b684 100644
--- a/docs/integrations/workos.mdx
+++ b/docs/integrations/workos.mdx
@@ -160,7 +160,11 @@ Your WorkOS AuthKit domain (e.g., `https://your-app.authkit.app`)
-Public URL of your FastMCP server for OAuth callbacks
+Public URL where OAuth endpoints will be accessible (includes any mount path)
+
+
+
+Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for details.
diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx
index 3ea550b0b..6dac4dadf 100644
--- a/docs/python-sdk/fastmcp-resources-template.mdx
+++ b/docs/python-sdk/fastmcp-resources-template.mdx
@@ -17,7 +17,7 @@ extract_query_params(uri_template: str) -> set[str]
```
-Extract query parameter names from RFC 6570 {?param1,param2} syntax.
+Extract query parameter names from RFC 6570 `{?param1,param2}` syntax.
### `build_regex`
@@ -30,9 +30,9 @@ build_regex(template: str) -> re.Pattern
Build regex pattern for URI template, handling RFC 6570 syntax.
Supports:
-- {var} - simple path parameter
-- {var*} - wildcard path parameter (captures multiple segments)
-- {?var1,var2} - query parameters (ignored in path matching)
+- `{var}` - simple path parameter
+- `{var*}` - wildcard path parameter (captures multiple segments)
+- `{?var1,var2}` - query parameters (ignored in path matching)
### `match_uri_template`
@@ -45,8 +45,8 @@ match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None
Match URI against template and extract both path and query parameters.
Supports RFC 6570 URI templates:
-- Path params: {var}, {var*}
-- Query params: {?var1,var2}
+- Path params: `{var}`, `{var*}`
+- Query params: `{?var1,var2}`
## Classes
diff --git a/docs/python-sdk/fastmcp-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx
index ae7f2f543..2e6fed379 100644
--- a/docs/python-sdk/fastmcp-server-auth-auth.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-auth.mdx
@@ -49,8 +49,9 @@ All auth providers must implement token verification.
get_routes(self, mcp_path: str | None = None) -> list[Route]
```
-Get the routes for this authentication provider.
+Get all routes for this authentication provider.
+This includes both well-known discovery routes and operational routes.
Each provider is responsible for creating whatever routes it needs:
- TokenVerifier: typically no routes (default implementation)
- RemoteAuthProvider: protected resource metadata routes
@@ -63,10 +64,38 @@ This is used to advertise the resource URL in metadata, but the
provider does not create the actual MCP endpoint route.
**Returns:**
-- List of routes for this provider (excluding the MCP endpoint itself)
+- List of all routes for this provider (excluding the MCP endpoint itself)
-#### `get_middleware`
+#### `get_well_known_routes`
+
+```python
+get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
+```
+
+Get well-known discovery routes for this authentication provider.
+
+This is a utility method that filters get_routes() to return only
+well-known discovery routes (those starting with /.well-known/).
+
+Well-known routes provide OAuth metadata and discovery endpoints that
+clients use to discover authentication capabilities. These routes should
+be mounted at the root level of the application to comply with RFC 8414
+and RFC 9728.
+
+Common well-known routes:
+- /.well-known/oauth-authorization-server (authorization server metadata)
+- /.well-known/oauth-protected-resource/* (protected resource metadata)
+
+**Args:**
+- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
+This is used to construct path-scoped well-known URLs.
+
+**Returns:**
+- List of well-known discovery routes (typically mounted at root level)
+
+
+#### `get_middleware`
```python
get_middleware(self) -> list
@@ -78,7 +107,7 @@ Get HTTP application-level middleware for this auth provider.
- List of Starlette Middleware instances to apply to the HTTP app
-### `TokenVerifier`
+### `TokenVerifier`
Base class for token verifiers (Resource Servers).
@@ -89,7 +118,7 @@ Token verifiers typically don't provide authentication routes by default.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -98,7 +127,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify a bearer token and return access info if valid.
-### `RemoteAuthProvider`
+### `RemoteAuthProvider`
Authentication provider for resource servers that verify tokens from known authorization servers.
@@ -115,7 +144,7 @@ the authorization servers that issue valid tokens.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -124,18 +153,18 @@ verify_token(self, token: str) -> AccessToken | None
Verify token using the configured token verifier.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
```
-Get OAuth routes for this provider.
+Get routes for this provider.
-Creates protected resource metadata routes.
+Creates protected resource metadata routes (RFC 9728).
-### `OAuthProvider`
+### `OAuthProvider`
OAuth Authorization Server provider.
@@ -146,7 +175,7 @@ authorization flows, token issuance, and token verification.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -164,7 +193,7 @@ to our existing load_access_token method.
- AccessToken object if valid, None if invalid or expired
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
index 9b363a46d..b383826b8 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
@@ -242,7 +242,7 @@ Handles provider-specific requirements:
**Methods:**
-#### `get_client`
+#### `get_client`
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@@ -254,7 +254,7 @@ provided to the DCR client during registration, not the upstream client ID.
For unregistered clients, returns None (which will raise an error in the SDK).
-#### `register_client`
+#### `register_client`
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@@ -268,7 +268,7 @@ redirect URI will likely be localhost or unknown to the proxied IDP. The
proxied IDP only knows about this server's fixed redirect URI.
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -282,7 +282,7 @@ Flow:
3. Consent handler redirects to upstream IdP if approved/already approved
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -294,7 +294,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
-#### `exchange_authorization_code`
+#### `exchange_authorization_code`
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@@ -306,7 +306,7 @@ For the DCR-compliant proxy flow, we return the IdP tokens that were obtained
during the IdP callback exchange. PKCE validation is handled by the MCP framework.
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@@ -315,7 +315,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str)
Load refresh token from local storage.
-#### `exchange_refresh_token`
+#### `exchange_refresh_token`
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@@ -324,7 +324,7 @@ exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token:
Exchange refresh token for new access token using authlib.
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -336,7 +336,7 @@ Delegates to the JWT verifier which handles signature validation,
expiration checking, and claims validation using the upstream JWKS.
-#### `revoke_token`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@@ -348,7 +348,7 @@ Removes tokens from local storage and attempts to revoke them with
the upstream server if a revocation endpoint is configured.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
index ed0df1307..f0124db1e 100644
--- a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
@@ -52,7 +52,7 @@ that is OIDC compliant.
**Methods:**
-#### `get_oidc_configuration`
+#### `get_oidc_configuration`
```python
get_oidc_configuration(self, config_url: AnyHttpUrl, strict: bool | None, timeout_seconds: int | None) -> OIDCConfiguration
@@ -66,7 +66,7 @@ Gets the OIDC configuration for the specified configuration URL.
- `timeout_seconds`: HTTP request timeout in seconds
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self) -> TokenVerifier
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx b/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
index fc58544d5..097662791 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
@@ -37,7 +37,7 @@ Example:
Settings for Auth0 OIDC provider.
-### `Auth0Provider`
+### `Auth0Provider`
An Auth0 provider implementation for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
index cf8063a9d..351732b17 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
@@ -37,7 +37,7 @@ Example:
Settings for AWS Cognito OAuth provider.
-### `AWSCognitoTokenVerifier`
+### `AWSCognitoTokenVerifier`
Token verifier that filters claims to Cognito-specific subset.
@@ -45,7 +45,7 @@ Token verifier that filters claims to Cognito-specific subset.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -54,7 +54,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify token and filter claims to Cognito-specific subset.
-### `AWSCognitoProvider`
+### `AWSCognitoProvider`
Complete AWS Cognito OAuth provider for FastMCP.
@@ -72,7 +72,7 @@ Features:
**Methods:**
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self) -> TokenVerifier
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index d44054dc7..d9748f0ed 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -20,7 +20,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
Settings for Azure OAuth provider.
-### `AzureProvider`
+### `AzureProvider`
Azure (Microsoft Entra) OAuth provider for FastMCP.
@@ -45,7 +45,7 @@ Setup:
**Methods:**
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
index 0a93964b3..5358f2817 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
@@ -35,7 +35,7 @@ Example:
Settings for GitHub OAuth provider.
-### `GitHubTokenVerifier`
+### `GitHubTokenVerifier`
Token verifier for GitHub OAuth tokens.
@@ -46,7 +46,7 @@ by calling GitHub's API to check if they're valid and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify GitHub OAuth token by calling GitHub API.
-### `GitHubProvider`
+### `GitHubProvider`
Complete GitHub OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
index 90ae29dab..006c22db3 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
@@ -35,7 +35,7 @@ Example:
Settings for Google OAuth provider.
-### `GoogleTokenVerifier`
+### `GoogleTokenVerifier`
Token verifier for Google OAuth tokens.
@@ -46,7 +46,7 @@ by calling Google's tokeninfo API to check if they're valid and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify Google OAuth token by calling Google's tokeninfo API.
-### `GoogleProvider`
+### `GoogleProvider`
Complete Google OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
index e38942ee1..60b8ccb67 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
@@ -24,7 +24,7 @@ Choose based on your WorkOS setup and authentication requirements.
Settings for WorkOS OAuth provider.
-### `WorkOSTokenVerifier`
+### `WorkOSTokenVerifier`
Token verifier for WorkOS OAuth tokens.
@@ -35,7 +35,7 @@ the /oauth2/userinfo endpoint to check validity and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -44,7 +44,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify WorkOS OAuth token by calling userinfo endpoint.
-### `WorkOSProvider`
+### `WorkOSProvider`
Complete WorkOS OAuth provider for FastMCP.
@@ -65,9 +65,9 @@ Setup Requirements:
4. Note your Client ID and Client Secret
-### `AuthKitProviderSettings`
+### `AuthKitProviderSettings`
-### `AuthKitProvider`
+### `AuthKitProvider`
AuthKit metadata provider for DCR (Dynamic Client Registration).
@@ -93,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx
index 6ded1069f..d06e214a0 100644
--- a/docs/python-sdk/fastmcp-server-http.mdx
+++ b/docs/python-sdk/fastmcp-server-http.mdx
@@ -7,13 +7,13 @@ sidebarTitle: http
## Functions
-### `set_http_request`
+### `set_http_request`
```python
set_http_request(request: Request) -> Generator[Request, None, None]
```
-### `create_base_app`
+### `create_base_app`
```python
create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan
@@ -32,7 +32,7 @@ Create a base Starlette app with common middleware and routes.
- A Starlette application
-### `create_sse_app`
+### `create_sse_app`
```python
create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: AuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
@@ -54,7 +54,7 @@ Returns:
A Starlette application with RequestContextMiddleware
-### `create_streamable_http_app`
+### `create_streamable_http_app`
```python
create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, auth: AuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
@@ -80,23 +80,23 @@ Return an instance of the StreamableHTTP server app.
## Classes
-### `StreamableHTTPASGIApp`
+### `StreamableHTTPASGIApp`
ASGI application wrapper for Streamable HTTP server transport.
-### `StarletteWithLifespan`
+### `StarletteWithLifespan`
**Methods:**
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> Lifespan[Starlette]
```
-### `RequestContextMiddleware`
+### `RequestContextMiddleware`
Middleware that stores each request in a ContextVar
diff --git a/docs/python-sdk/fastmcp-server-middleware-caching.mdx b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
new file mode 100644
index 000000000..a0c837507
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
@@ -0,0 +1,189 @@
+---
+title: caching
+sidebarTitle: caching
+---
+
+# `fastmcp.server.middleware.caching`
+
+
+A middleware for response caching.
+
+## Classes
+
+### `CachableReadResourceContents`
+
+
+A wrapper for ReadResourceContents that can be cached.
+
+
+**Methods:**
+
+#### `get_size`
+
+```python
+get_size(self) -> int
+```
+
+#### `get_sizes`
+
+```python
+get_sizes(cls, values: Sequence[Self]) -> int
+```
+
+#### `wrap`
+
+```python
+wrap(cls, values: Sequence[ReadResourceContents]) -> list[Self]
+```
+
+#### `unwrap`
+
+```python
+unwrap(cls, values: Sequence[Self]) -> list[ReadResourceContents]
+```
+
+### `CachableToolResult`
+
+**Methods:**
+
+#### `wrap`
+
+```python
+wrap(cls, value: ToolResult) -> Self
+```
+
+#### `unwrap`
+
+```python
+unwrap(self) -> ToolResult
+```
+
+### `SharedMethodSettings`
+
+
+Shared config for a cache method.
+
+
+### `ListToolsSettings`
+
+
+Configuration options for Tool-related caching.
+
+
+### `ListResourcesSettings`
+
+
+Configuration options for Resource-related caching.
+
+
+### `ListPromptsSettings`
+
+
+Configuration options for Prompt-related caching.
+
+
+### `CallToolSettings`
+
+
+Configuration options for Tool-related caching.
+
+
+### `ReadResourceSettings`
+
+
+Configuration options for Resource-related caching.
+
+
+### `GetPromptSettings`
+
+
+Configuration options for Prompt-related caching.
+
+
+### `ResponseCachingStatistics`
+
+### `ResponseCachingMiddleware`
+
+
+The response caching middleware offers a simple way to cache responses to mcp methods. The Middleware
+supports cache invalidation via notifications from the server. The Middleware implements TTL-based caching
+but cache implementations may offer additional features like LRU eviction, size limits, and more.
+
+When items are retrieved from the cache they will no longer be the original objects, but rather no-op objects
+this means that response caching may not be compatible with other middleware that expects original subclasses.
+
+Notes:
+- Caches `tools/call`, `resources/read`, `prompts/get`, `tools/list`, `resources/list`, and `prompts/list` requests.
+- Cache keys are derived from method name and arguments.
+
+
+**Methods:**
+
+#### `on_list_tools`
+
+```python
+on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
+```
+
+List tools from the cache, if caching is enabled, and the result is in the cache. Otherwise,
+otherwise call the next middleware and store the result in the cache if caching is enabled.
+
+
+#### `on_list_resources`
+
+```python
+on_list_resources(self, context: MiddlewareContext[mcp.types.ListResourcesRequest], call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
+```
+
+List resources from the cache, if caching is enabled, and the result is in the cache. Otherwise,
+otherwise call the next middleware and store the result in the cache if caching is enabled.
+
+
+#### `on_list_prompts`
+
+```python
+on_list_prompts(self, context: MiddlewareContext[mcp.types.ListPromptsRequest], call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
+```
+
+List prompts from the cache, if caching is enabled, and the result is in the cache. Otherwise,
+otherwise call the next middleware and store the result in the cache if caching is enabled.
+
+
+#### `on_call_tool`
+
+```python
+on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
+```
+
+Call a tool from the cache, if caching is enabled, and the result is in the cache. Otherwise,
+otherwise call the next middleware and store the result in the cache if caching is enabled.
+
+
+#### `on_read_resource`
+
+```python
+on_read_resource(self, context: MiddlewareContext[mcp.types.ReadResourceRequestParams], call_next: CallNext[mcp.types.ReadResourceRequestParams, Sequence[ReadResourceContents]]) -> Sequence[ReadResourceContents]
+```
+
+Read a resource from the cache, if caching is enabled, and the result is in the cache. Otherwise,
+otherwise call the next middleware and store the result in the cache if caching is enabled.
+
+
+#### `on_get_prompt`
+
+```python
+on_get_prompt(self, context: MiddlewareContext[mcp.types.GetPromptRequestParams], call_next: CallNext[mcp.types.GetPromptRequestParams, mcp.types.GetPromptResult]) -> mcp.types.GetPromptResult
+```
+
+Get a prompt from the cache, if caching is enabled, and the result is in the cache. Otherwise,
+otherwise call the next middleware and store the result in the cache if caching is enabled.
+
+
+#### `statistics`
+
+```python
+statistics(self) -> ResponseCachingStatistics
+```
+
+Get the statistics for the cache.
+
diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index 47ce45616..43c2858f1 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -110,7 +110,9 @@ mcp = FastMCP(name="My Server", auth=auth)
- Public URL of your FastMCP server (e.g., `https://your-server.com`)
+ Public URL where OAuth endpoints will be accessible, **including any mount path** (e.g., `https://your-server.com/api`).
+
+ This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to specify where auth server metadata is located (typically at root level).
@@ -123,7 +125,25 @@ mcp = FastMCP(name="My Server", auth=auth)
- Issuer URL for OAuth metadata (defaults to base_url)
+ Issuer URL for OAuth authorization server metadata (defaults to `base_url`).
+
+ When mounting your MCP server under a path prefix (e.g., `/api`), set this to your root-level URL to avoid 404 logs during OAuth discovery. MCP clients try path-scoped discovery first per RFC 8414, which will fail if your auth server metadata is at the root level.
+
+ **Example with mounting:**
+ ```python
+ auth = GitHubProvider(
+ base_url="http://localhost:8000/api", # OAuth endpoints under /api
+ issuer_url="http://localhost:8000" # Auth server metadata at root
+ )
+ ```
+
+ Without `issuer_url`, clients will attempt `/.well-known/oauth-authorization-server/api` (404) before falling back to `/.well-known/oauth-authorization-server` (success). Setting `issuer_url` to the root eliminates the 404 attempt.
+
+ **When to use:**
+ - **Default (`None`)**: Use `base_url` as issuer - simple deployments at root path
+ - **Root-level URL**: Mounting under a path prefix - avoids 404 logs
+
+ See the [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for complete mounting examples.
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index f4a5f1ae6..982dc25ff 100644
--- a/docs/servers/server.mdx
+++ b/docs/servers/server.mdx
@@ -242,7 +242,7 @@ Custom routes are served alongside your MCP endpoint and are useful for:
- Simple status or info endpoints
- Basic webhooks or callbacks
-For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/self-hosted#integration-with-web-frameworks).
+For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
## Composing Servers
diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py
index 3e249c3d6..a604b124a 100644
--- a/src/fastmcp/resources/template.py
+++ b/src/fastmcp/resources/template.py
@@ -27,7 +27,7 @@ from fastmcp.utilities.types import (
def extract_query_params(uri_template: str) -> set[str]:
- """Extract query parameter names from RFC 6570 {?param1,param2} syntax."""
+ """Extract query parameter names from RFC 6570 `{?param1,param2}` syntax."""
match = re.search(r"\{\?([^}]+)\}", uri_template)
if match:
return {p.strip() for p in match.group(1).split(",")}
@@ -38,9 +38,9 @@ def build_regex(template: str) -> re.Pattern:
"""Build regex pattern for URI template, handling RFC 6570 syntax.
Supports:
- - {var} - simple path parameter
- - {var*} - wildcard path parameter (captures multiple segments)
- - {?var1,var2} - query parameters (ignored in path matching)
+ - `{var}` - simple path parameter
+ - `{var*}` - wildcard path parameter (captures multiple segments)
+ - `{?var1,var2}` - query parameters (ignored in path matching)
"""
# Remove query parameter syntax for path matching
template_without_query = re.sub(r"\{\?[^}]+\}", "", template)
@@ -64,8 +64,8 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
"""Match URI against template and extract both path and query parameters.
Supports RFC 6570 URI templates:
- - Path params: {var}, {var*}
- - Query params: {?var1,var2}
+ - Path params: `{var}`, `{var*}`
+ - Query params: `{?var1,var2}`
"""
# Split URI into path and query parts
uri_path, _, query_string = uri.partition("?")
diff --git a/src/fastmcp/server/auth/auth.py b/src/fastmcp/server/auth/auth.py
index de545c7c2..2bec554f6 100644
--- a/src/fastmcp/server/auth/auth.py
+++ b/src/fastmcp/server/auth/auth.py
@@ -79,8 +79,9 @@ class AuthProvider(TokenVerifierProtocol):
self,
mcp_path: str | None = None,
) -> list[Route]:
- """Get the routes for this authentication provider.
+ """Get all routes for this authentication provider.
+ This includes both well-known discovery routes and operational routes.
Each provider is responsible for creating whatever routes it needs:
- TokenVerifier: typically no routes (default implementation)
- RemoteAuthProvider: protected resource metadata routes
@@ -93,10 +94,42 @@ class AuthProvider(TokenVerifierProtocol):
provider does not create the actual MCP endpoint route.
Returns:
- List of routes for this provider (excluding the MCP endpoint itself)
+ List of all routes for this provider (excluding the MCP endpoint itself)
"""
return []
+ def get_well_known_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get well-known discovery routes for this authentication provider.
+
+ This is a utility method that filters get_routes() to return only
+ well-known discovery routes (those starting with /.well-known/).
+
+ Well-known routes provide OAuth metadata and discovery endpoints that
+ clients use to discover authentication capabilities. These routes should
+ be mounted at the root level of the application to comply with RFC 8414
+ and RFC 9728.
+
+ Common well-known routes:
+ - /.well-known/oauth-authorization-server (authorization server metadata)
+ - /.well-known/oauth-protected-resource/* (protected resource metadata)
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to construct path-scoped well-known URLs.
+
+ Returns:
+ List of well-known discovery routes (typically mounted at root level)
+ """
+ all_routes = self.get_routes(mcp_path)
+ return [
+ route
+ for route in all_routes
+ if isinstance(route, Route) and route.path.startswith("/.well-known/")
+ ]
+
def get_middleware(self) -> list:
"""Get HTTP application-level middleware for this auth provider.
@@ -205,12 +238,11 @@ class RemoteAuthProvider(AuthProvider):
self,
mcp_path: str | None = None,
) -> list[Route]:
- """Get OAuth routes for this provider.
+ """Get routes for this provider.
- Creates protected resource metadata routes.
+ Creates protected resource metadata routes (RFC 9728).
"""
- # Start with base routes
- routes = super().get_routes(mcp_path)
+ routes = []
# Get the resource URL based on the MCP path
resource_url = self._get_resource_url(mcp_path)
diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py
index e79df5753..59396850f 100644
--- a/src/fastmcp/server/auth/oauth_proxy.py
+++ b/src/fastmcp/server/auth/oauth_proxy.py
@@ -583,6 +583,13 @@ class OAuthProxy(OAuthProvider):
"For production, configure persistent storage (Redis, PostgreSQL, etc.)."
)
+ # Cache HTTPS check to avoid repeated logging
+ self._is_https = str(self.base_url).startswith("https://")
+ if not self._is_https:
+ logger.warning(
+ "Using non-secure cookies for development; deploy with HTTPS for production."
+ )
+
self._client_store = PydanticAdapter[ProxyDCRClient](
key_value=self._client_storage,
pydantic_model=ProxyDCRClient,
@@ -1264,12 +1271,8 @@ class OAuthProxy(OAuthProvider):
def _cookie_name(self, base_name: str) -> str:
"""Return secure cookie name for HTTPS, fallback for HTTP development."""
- base_url_str = str(self.base_url)
- if base_url_str.startswith("https://"):
+ if self._is_https:
return f"__Host-{base_name}"
- logger.warning(
- "Using non-secure cookies for development; deploy with HTTPS for production."
- )
return f"__{base_name}"
def _sign_cookie(self, payload: str) -> str:
@@ -1346,12 +1349,11 @@ class OAuthProxy(OAuthProvider):
max_age: int,
) -> None:
name = self._cookie_name(base_name)
- secure = str(self.base_url).startswith("https://")
response.set_cookie(
name,
value_b64,
max_age=max_age,
- secure=secure,
+ secure=self._is_https,
httponly=True,
samesite="lax",
path="/",
diff --git a/src/fastmcp/server/auth/oidc_proxy.py b/src/fastmcp/server/auth/oidc_proxy.py
index 589e0e2d3..6d5c82fe8 100644
--- a/src/fastmcp/server/auth/oidc_proxy.py
+++ b/src/fastmcp/server/auth/oidc_proxy.py
@@ -210,6 +210,7 @@ class OIDCProxy(OAuthProxy):
required_scopes: list[str] | None = None,
# FastMCP server configuration
base_url: AnyHttpUrl | str,
+ issuer_url: AnyHttpUrl | str | None = None,
redirect_path: str | None = None,
# Client configuration
allowed_client_redirect_uris: list[str] | None = None,
@@ -228,8 +229,9 @@ class OIDCProxy(OAuthProxy):
timeout_seconds: HTTP request timeout in seconds
algorithm: Token verifier algorithm
required_scopes: Required OAuth scopes
- base_url: Public URL of the server that exposes this FastMCP server; redirect path is
- relative to this URL
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
@@ -289,6 +291,7 @@ class OIDCProxy(OAuthProxy):
"upstream_revocation_endpoint": revocation_endpoint,
"token_verifier": token_verifier,
"base_url": base_url,
+ "issuer_url": issuer_url or base_url,
"service_documentation_url": self.oidc_config.service_documentation,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"client_storage": client_storage,
diff --git a/src/fastmcp/server/auth/providers/auth0.py b/src/fastmcp/server/auth/providers/auth0.py
index 24d3020b6..4d994ce98 100644
--- a/src/fastmcp/server/auth/providers/auth0.py
+++ b/src/fastmcp/server/auth/providers/auth0.py
@@ -48,6 +48,7 @@ class Auth0ProviderSettings(BaseSettings):
client_secret: SecretStr | None = None
audience: str | None = None
base_url: AnyHttpUrl | None = None
+ issuer_url: AnyHttpUrl | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
allowed_client_redirect_uris: list[str] | None = None
@@ -90,6 +91,7 @@ class Auth0Provider(OIDCProxy):
client_secret: str | NotSetT = NotSet,
audience: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
@@ -102,7 +104,9 @@ class Auth0Provider(OIDCProxy):
client_id: Auth0 application client id
client_secret: Auth0 application client secret
audience: Auth0 API audience
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
required_scopes: Required Auth0 scopes (defaults to ["openid"])
redirect_path: Redirect path configured in Auth0 application
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
@@ -118,6 +122,7 @@ class Auth0Provider(OIDCProxy):
"client_secret": client_secret,
"audience": audience,
"base_url": base_url,
+ "issuer_url": issuer_url,
"required_scopes": required_scopes,
"redirect_path": redirect_path,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
@@ -159,6 +164,7 @@ class Auth0Provider(OIDCProxy):
"client_secret": settings.client_secret.get_secret_value(),
"audience": settings.audience,
"base_url": settings.base_url,
+ "issuer_url": settings.issuer_url,
"redirect_path": settings.redirect_path,
"required_scopes": auth0_required_scopes,
"allowed_client_redirect_uris": settings.allowed_client_redirect_uris,
diff --git a/src/fastmcp/server/auth/providers/aws.py b/src/fastmcp/server/auth/providers/aws.py
index 6569b6d3d..31de6c9a0 100644
--- a/src/fastmcp/server/auth/providers/aws.py
+++ b/src/fastmcp/server/auth/providers/aws.py
@@ -53,6 +53,7 @@ class AWSCognitoProviderSettings(BaseSettings):
client_id: str | None = None
client_secret: SecretStr | None = None
base_url: AnyHttpUrl | str | None = None
+ issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
allowed_client_redirect_uris: list[str] | None = None
@@ -129,6 +130,7 @@ class AWSCognitoProvider(OIDCProxy):
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
@@ -141,7 +143,9 @@ class AWSCognitoProvider(OIDCProxy):
aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
client_id: Cognito app client ID
client_secret: Cognito app client secret
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
required_scopes: Required Cognito scopes (defaults to ["openid"])
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
@@ -158,6 +162,7 @@ class AWSCognitoProvider(OIDCProxy):
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
+ "issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
@@ -206,6 +211,7 @@ class AWSCognitoProvider(OIDCProxy):
algorithm="RS256",
required_scopes=required_scopes_final,
base_url=settings.base_url,
+ issuer_url=settings.issuer_url,
redirect_path=redirect_path_final,
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
client_storage=client_storage,
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
index 723d79152..93ad94182 100644
--- a/src/fastmcp/server/auth/providers/azure.py
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -40,6 +40,7 @@ class AzureProviderSettings(BaseSettings):
tenant_id: str | None = None
identifier_uri: str | None = None
base_url: str | None = None
+ issuer_url: str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
additional_authorize_scopes: list[str] | None = None
@@ -102,6 +103,7 @@ class AzureProvider(OAuthProxy):
tenant_id: str | NotSetT = NotSet,
identifier_uri: str | None | NotSetT = NotSet,
base_url: str | NotSetT = NotSet,
+ issuer_url: str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
@@ -117,7 +119,9 @@ class AzureProvider(OAuthProxy):
identifier_uri: Optional Application ID URI for your API. (defaults to api://{client_id})
Used only to prefix scopes in authorization requests. Tokens are always validated
against your app's client ID.
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
required_scopes: Required scopes. These are validated on tokens and used as defaults
when the client does not request specific scopes.
@@ -137,6 +141,7 @@ class AzureProvider(OAuthProxy):
"tenant_id": tenant_id,
"identifier_uri": identifier_uri,
"base_url": base_url,
+ "issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"additional_authorize_scopes": additional_authorize_scopes,
@@ -207,7 +212,8 @@ class AzureProvider(OAuthProxy):
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
- issuer_url=settings.base_url,
+ issuer_url=settings.issuer_url
+ or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
client_storage=client_storage,
)
diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py
index 0846bd03f..d34bf041d 100644
--- a/src/fastmcp/server/auth/providers/github.py
+++ b/src/fastmcp/server/auth/providers/github.py
@@ -49,6 +49,7 @@ class GitHubProviderSettings(BaseSettings):
client_id: str | None = None
client_secret: SecretStr | None = None
base_url: AnyHttpUrl | str | None = None
+ issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
@@ -199,6 +200,7 @@ class GitHubProvider(OAuthProxy):
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
@@ -210,7 +212,9 @@ class GitHubProvider(OAuthProxy):
Args:
client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
client_secret: GitHub OAuth app client secret
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
required_scopes: Required GitHub scopes (defaults to ["user"])
timeout_seconds: HTTP request timeout for GitHub API calls
@@ -226,6 +230,7 @@ class GitHubProvider(OAuthProxy):
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
+ "issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
@@ -271,7 +276,8 @@ class GitHubProvider(OAuthProxy):
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
- issuer_url=settings.base_url, # We act as the issuer for client registration
+ issuer_url=settings.issuer_url
+ or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
client_storage=client_storage,
)
diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py
index 71cb29472..12bdce5d8 100644
--- a/src/fastmcp/server/auth/providers/google.py
+++ b/src/fastmcp/server/auth/providers/google.py
@@ -51,6 +51,7 @@ class GoogleProviderSettings(BaseSettings):
client_id: str | None = None
client_secret: SecretStr | None = None
base_url: AnyHttpUrl | str | None = None
+ issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
@@ -215,6 +216,7 @@ class GoogleProvider(OAuthProxy):
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
@@ -226,7 +228,9 @@ class GoogleProvider(OAuthProxy):
Args:
client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...")
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Google OAuth app (defaults to "/auth/callback")
required_scopes: Required Google scopes (defaults to ["openid"]). Common scopes include:
- "openid" for OpenID Connect (default)
@@ -245,6 +249,7 @@ class GoogleProvider(OAuthProxy):
"client_id": client_id,
"client_secret": client_secret,
"base_url": base_url,
+ "issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
@@ -290,7 +295,8 @@ class GoogleProvider(OAuthProxy):
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
- issuer_url=settings.base_url, # We act as the issuer for client registration
+ issuer_url=settings.issuer_url
+ or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
client_storage=client_storage,
)
diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py
index ae8814a92..52b6c62d2 100644
--- a/src/fastmcp/server/auth/providers/workos.py
+++ b/src/fastmcp/server/auth/providers/workos.py
@@ -41,6 +41,7 @@ class WorkOSProviderSettings(BaseSettings):
client_secret: SecretStr | None = None
authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
base_url: AnyHttpUrl | str | None = None
+ issuer_url: AnyHttpUrl | str | None = None
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
@@ -165,6 +166,7 @@ class WorkOSProvider(OAuthProxy):
client_secret: str | NotSetT = NotSet,
authkit_domain: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
@@ -177,7 +179,9 @@ class WorkOSProvider(OAuthProxy):
client_id: WorkOS client ID
client_secret: WorkOS client secret
authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
required_scopes: Required OAuth scopes (no default)
timeout_seconds: HTTP request timeout for WorkOS API calls
@@ -194,6 +198,7 @@ class WorkOSProvider(OAuthProxy):
"client_secret": client_secret,
"authkit_domain": authkit_domain,
"base_url": base_url,
+ "issuer_url": issuer_url,
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
@@ -247,7 +252,8 @@ class WorkOSProvider(OAuthProxy):
token_verifier=token_verifier,
base_url=settings.base_url,
redirect_path=settings.redirect_path,
- issuer_url=settings.base_url,
+ issuer_url=settings.issuer_url
+ or settings.base_url, # Default to base_url if not specified
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
client_storage=client_storage,
)
diff --git a/tests/server/auth/test_oauth_mounting.py b/tests/server/auth/test_oauth_mounting.py
new file mode 100644
index 000000000..28d72e2ae
--- /dev/null
+++ b/tests/server/auth/test_oauth_mounting.py
@@ -0,0 +1,196 @@
+"""Tests for OAuth .well-known routes when FastMCP apps are mounted in parent ASGI apps.
+
+This test file validates the fix for issue #2077 where .well-known/oauth-protected-resource
+returns 404 at root level when a FastMCP app is mounted under a path prefix.
+
+The fix uses MCP SDK 1.17+ which implements RFC 9728 path-scoped well-known URLs.
+"""
+
+import httpx
+import pytest
+from pydantic import AnyHttpUrl
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+from fastmcp import FastMCP
+from fastmcp.server.auth import RemoteAuthProvider
+from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
+
+
+@pytest.fixture
+def test_tokens():
+ """Standard test tokens fixture."""
+ return {
+ "test_token": {
+ "client_id": "test-client",
+ "scopes": ["read", "write"],
+ }
+ }
+
+
+class TestOAuthMounting:
+ """Test OAuth .well-known routes with mounted FastMCP apps."""
+
+ async def test_well_known_with_direct_deployment(self, test_tokens):
+ """Test that .well-known routes work when app is deployed directly (not mounted).
+
+ This is the baseline - it should work as expected.
+ Per RFC 9728, if the resource is at /mcp, the well-known endpoint is at
+ /.well-known/oauth-protected-resource/mcp (path-scoped).
+ """
+ token_verifier = StaticTokenVerifier(tokens=test_tokens)
+ auth_provider = RemoteAuthProvider(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
+ base_url="https://api.example.com",
+ )
+
+ mcp = FastMCP("test-server", auth=auth_provider)
+ mcp_app = mcp.http_app()
+
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=mcp_app),
+ base_url="https://api.example.com",
+ ) as client:
+ # RFC 9728: path-scoped well-known URL
+ # Resource is at /mcp, so well-known should be at /.well-known/oauth-protected-resource/mcp
+ response = await client.get("/.well-known/oauth-protected-resource/mcp")
+ assert response.status_code == 200
+
+ data = response.json()
+ assert data["resource"] == "https://api.example.com/mcp"
+ assert data["authorization_servers"] == ["https://auth.example.com/"]
+
+ async def test_well_known_with_mounted_app(self, test_tokens):
+ """Test that .well-known routes work when explicitly mounted at root.
+
+ This test uses the CANONICAL pattern for mounting:
+ - base_url includes the mount prefix ("/api")
+ - mcp_path is just the internal MCP path ("/mcp")
+ - These combine: base_url + mcp_path = actual URL
+
+ The well-known routes are mounted at root level for RFC 9728 compliance.
+ """
+ token_verifier = StaticTokenVerifier(tokens=test_tokens)
+ # CANONICAL PATTERN: base_url includes the mount prefix
+ auth_provider = RemoteAuthProvider(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
+ base_url="https://api.example.com/api", # Includes /api mount prefix
+ )
+
+ mcp = FastMCP("test-server", auth=auth_provider)
+ mcp_app = mcp.http_app(path="/mcp")
+
+ # Pass just the internal mcp_path, NOT the full mount path
+ # The auth provider will combine base_url + mcp_path internally
+ well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
+
+ parent_app = Starlette(
+ routes=[
+ *well_known_routes, # Well-known routes at root level
+ Mount("/api", app=mcp_app), # MCP app under /api
+ ],
+ lifespan=mcp_app.lifespan,
+ )
+
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=parent_app),
+ base_url="https://api.example.com",
+ ) as client:
+ # The CORRECT RFC 9728 path-scoped well-known URL at root
+ # Resource is at /api/mcp, so well-known is at /.well-known/oauth-protected-resource/api/mcp
+ response = await client.get("/.well-known/oauth-protected-resource/api/mcp")
+ assert response.status_code == 200
+
+ data = response.json()
+ assert data["resource"] == "https://api.example.com/api/mcp"
+ assert data["authorization_servers"] == ["https://auth.example.com/"]
+
+ # There will also be an extra route at /api/.well-known/oauth-protected-resource/mcp
+ # (from the mounted MCP app), but we don't care about that as long as the correct one exists
+
+ async def test_mcp_endpoint_with_mounted_app(self, test_tokens):
+ """Test that MCP endpoint works correctly when mounted.
+
+ This confirms the MCP functionality itself works with mounting.
+ """
+ token_verifier = StaticTokenVerifier(tokens=test_tokens)
+ auth_provider = RemoteAuthProvider(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
+ base_url="https://api.example.com",
+ )
+
+ mcp = FastMCP("test-server", auth=auth_provider)
+
+ @mcp.tool
+ def test_tool(message: str) -> str:
+ return f"Echo: {message}"
+
+ mcp_app = mcp.http_app(path="/mcp")
+
+ # Mount the MCP app under /api prefix
+ parent_app = Starlette(
+ routes=[
+ Mount("/api", app=mcp_app),
+ ],
+ lifespan=mcp_app.lifespan,
+ )
+
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=parent_app),
+ base_url="https://api.example.com",
+ ) as client:
+ # The MCP endpoint should work at /api/mcp (mounted correctly)
+ # This is a basic connectivity test
+ response = await client.get("/api/mcp")
+
+ # We expect either 200 (if no auth required for GET) or 401 (if auth required)
+ # The key is that it's NOT 404
+ assert response.status_code in [200, 401, 405]
+
+ async def test_nested_mounting(self, test_tokens):
+ """Test .well-known routes with deeply nested mounts.
+
+ Uses CANONICAL pattern: base_url includes all mount prefixes.
+ """
+ token_verifier = StaticTokenVerifier(tokens=test_tokens)
+ # CANONICAL PATTERN: base_url includes full mount path /outer/inner
+ auth_provider = RemoteAuthProvider(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
+ base_url="https://api.example.com/outer/inner", # Includes nested mount path
+ )
+
+ mcp = FastMCP("test-server", auth=auth_provider)
+ mcp_app = mcp.http_app(path="/mcp")
+
+ # Pass just the internal mcp_path
+ well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
+
+ # Create nested mounts
+ inner_app = Starlette(
+ routes=[Mount("/inner", app=mcp_app)],
+ )
+ outer_app = Starlette(
+ routes=[
+ *well_known_routes, # Well-known routes at root level
+ Mount("/outer", app=inner_app),
+ ],
+ lifespan=mcp_app.lifespan,
+ )
+
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=outer_app),
+ base_url="https://api.example.com",
+ ) as client:
+ # RFC 9728: path-scoped well-known URL for nested mounting
+ # Resource is at /outer/inner/mcp, so well-known is at /.well-known/oauth-protected-resource/outer/inner/mcp
+ response = await client.get(
+ "/.well-known/oauth-protected-resource/outer/inner/mcp"
+ )
+ assert response.status_code == 200
+
+ data = response.json()
+ assert data["resource"] == "https://api.example.com/outer/inner/mcp"
diff --git a/tests/server/auth/test_remote_auth_provider.py b/tests/server/auth/test_remote_auth_provider.py
index 0ef2cadfd..0419493f1 100644
--- a/tests/server/auth/test_remote_auth_provider.py
+++ b/tests/server/auth/test_remote_auth_provider.py
@@ -78,6 +78,7 @@ class TestRemoteAuthProvider:
assert len(routes) == 1
# Check that the route is the OAuth protected resource metadata endpoint
+ # When called without mcp_path, it creates route at /.well-known/oauth-protected-resource
route = routes[0]
assert route.path == "/.well-known/oauth-protected-resource"
assert route.methods is not None
@@ -99,10 +100,10 @@ class TestRemoteAuthProvider:
)
metadata_url = provider._get_resource_url(
- "/.well-known/oauth-protected-resource"
+ "/.well-known/oauth-protected-resource/mcp"
)
assert metadata_url == AnyHttpUrl(
- "https://api.example.com/.well-known/oauth-protected-resource"
+ "https://api.example.com/.well-known/oauth-protected-resource/mcp"
)
def test_get_resource_url_with_nested_base_url(self):
@@ -121,10 +122,10 @@ class TestRemoteAuthProvider:
)
metadata_url = provider._get_resource_url(
- "/.well-known/oauth-protected-resource"
+ "/.well-known/oauth-protected-resource/mcp"
)
assert metadata_url == AnyHttpUrl(
- "https://api.example.com/v1/.well-known/oauth-protected-resource"
+ "https://api.example.com/v1/.well-known/oauth-protected-resource/mcp"
)
def test_get_resource_url_handles_trailing_slash(self):
@@ -143,10 +144,10 @@ class TestRemoteAuthProvider:
)
metadata_url = provider._get_resource_url(
- "/.well-known/oauth-protected-resource"
+ "/.well-known/oauth-protected-resource/mcp"
)
assert metadata_url == AnyHttpUrl(
- "https://api.example.com/.well-known/oauth-protected-resource"
+ "https://api.example.com/.well-known/oauth-protected-resource/mcp"
)