Commit 3682b37
authored
fix: Always put the well-known endpoints at the server root
# Bug Report: Incorrect `.well-known/oauth-protected-resource` endpoint path when `resource_server_url` ends with `/sse`
## Summary
When configuring FastMCP with OAuth2 authentication and setting the `resource_server_url` to end with `/sse` (as required by VSCode MCP clients), FastMCP incorrectly serves the `.well-known/oauth-protected-resource` endpoint at `/sse/.well-known/oauth-protected-resource` instead of the expected root path `/.well-known/oauth-protected-resource`.
## Environment
- **FastMCP version**: Part of `mcp` Python library (check with `pip show mcp`)
- **Python version**: 3.13
- **Operating System**: macOS
- **MCP Client**: VSCode with MCP extension
- **Affected Files**:
- `mcp/server/fastmcp/server.py` (lines ~790-797)
- `mcp/server/auth/routes.py` (lines ~215-224)
## Expected Behavior
1. The `.well-known/oauth-protected-resource` endpoint should always be served at the root path (`/.well-known/oauth-protected-resource`) regardless of the `resource_server_url` configuration
2. The `resource` field in the `.well-known` response should point to the actual protected resource (e.g., `/sse`)
3. OAuth2 discovery should work correctly with MCP clients like VSCode
## Actual Behavior
When `resource_server_url` is set to `http://localhost:8099/sse`, FastMCP:
1. Serves `.well-known/oauth-protected-resource` at `/sse/.well-known/oauth-protected-resource`
2. The SSE endpoint `/sse` returns a `www-authenticate` header with `resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"`
3. OAuth2 discovery fails because clients expect the `.well-known` endpoint at the root
## Steps to Reproduce
1. Create a FastMCP server with OAuth2 configuration:
```python
from mcp.server.fastmcp import FastMCP
from mcp.server.auth.settings import AuthSettings
from pydantic import AnyHttpUrl
# Configure auth settings with /sse endpoint
auth_settings = AuthSettings(
issuer_url=AnyHttpUrl("https://login.microsoftonline.com/tenant-id/v2.0"),
resource_server_url=AnyHttpUrl("http://localhost:8099/sse"), # Note: ends with /sse
required_scopes=["https://example.com/scope"]
)
mcp = FastMCP(
"Test Server",
token_verifier=your_token_verifier,
auth=auth_settings,
)
app = mcp.sse_app()
```
2. Start the server: `uvicorn server:app --port 8099`
3. Test the endpoints:
```bash
# This should work but returns 404
curl http://localhost:8099/.well-known/oauth-protected-resource
# This works but shouldn't be the location
curl http://localhost:8099/sse/.well-known/oauth-protected-resource
# SSE endpoint references wrong .well-known location
curl -I http://localhost:8099/sse
# Returns: resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"
```
## Root Cause Analysis
**Exact Location of Bug**:
- **File**: `mcp/server/fastmcp/server.py`
- **Lines**: ~790-797 in the `sse_app()` method
- **Function**: `FastMCP.sse_app()`
**The Issue**: When setting up OAuth2 authentication, FastMCP constructs the `resource_metadata_url` incorrectly:
```python
# BUGGY CODE - Line ~790-797 in sse_app() method
resource_metadata_url = AnyHttpUrl(
str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
)
```
When `resource_server_url` is `http://localhost:8099/sse`, this creates `http://localhost:8099/sse/.well-known/oauth-protected-resource`.
However, the actual `.well-known` endpoint is created by `create_protected_resource_routes()` (in `mcp/server/auth/routes.py` lines ~215-224), which always creates it at the root path:
```python
# CORRECT CODE - This always creates /.well-known/oauth-protected-resource at root
return [
Route(
"/.well-known/oauth-protected-resource",
endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]),
methods=["GET", "OPTIONS"],
)
]
```
**The Fix**: The `resource_metadata_url` should be constructed from the base URL, not the `resource_server_url`:
```python
# PROPOSED FIX
if self.settings.auth and self.settings.auth.resource_server_url:
from pydantic import AnyHttpUrl
from urllib.parse import urlparse
# Extract base URL from resource_server_url
parsed = urlparse(str(self.settings.auth.resource_server_url))
base_url = f"{parsed.scheme}://{parsed.netloc}"
resource_metadata_url = AnyHttpUrl(
base_url + "/.well-known/oauth-protected-resource"
)
```
## Impact
- **High**: Breaks OAuth2 discovery for MCP clients like VSCode
- MCP servers cannot be properly authenticated when using the recommended `/sse` resource URL pattern
- Workarounds require custom endpoint overrides, defeating the purpose of built-in auth support
## Proposed Solution
The `.well-known/oauth-protected-resource` endpoint should always be served at the root path (`/.well-known/oauth-protected-resource`), regardless of the `resource_server_url` configuration. The `resource_server_url` should only affect:
1. The `resource` field value in the `.well-known` response
2. The `resource_metadata` reference in `www-authenticate` headers
## Current Workaround
Override the built-in `.well-known` endpoint with a custom implementation:
```python
async def custom_well_known_endpoint(request):
return JSONResponse({
"resource": f"{config.EXTERNAL_ADDRESS}/sse",
"authorization_servers": ["https://login.microsoftonline.com/tenant/v2.0"],
"scopes_supported": ["https://example.com/scope"],
"bearer_methods_supported": ["header"]
})
# Override the built-in endpoint
app.router.routes.insert(0, Route("/.well-known/oauth-protected-resource", custom_well_known_endpoint, methods=["GET"]))
```
## Additional Context
- This issue specifically affects integration with VSCode MCP clients, which require the resource URL to end with `/sse`
- The OAuth2 specification (RFC 8414) defines `.well-known` endpoints should be at predictable root paths
- Other OAuth2 implementations (e.g., Auth0, Okta) serve `.well-known` endpoints at root regardless of resource configuration
## Related Documentation
- [RFC 8414 - OAuth 2.0 Authorization Server Metadata](https://tools.ietf.org/html/rfc8414)
- [MCP OAuth2 Authentication Documentation](https://modelcontextprotocol.io/docs/concepts/authentication)1 parent a82c69b commit 3682b37
1 file changed
+7
-3
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
792 | 792 | | |
793 | 793 | | |
794 | 794 | | |
795 | | - | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
796 | 801 | | |
797 | | - | |
| 802 | + | |
798 | 803 | | |
799 | | - | |
800 | 804 | | |
801 | 805 | | |
802 | 806 | | |
| |||
0 commit comments