fix: Add CORS middleware to allow browser-based clients like MCP Inspector
This commit is contained in:
@@ -11,6 +11,7 @@ from mcp.server.auth.settings import AuthSettings
|
|||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
from pydantic import AnyHttpUrl
|
from pydantic import AnyHttpUrl
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette.routing import Mount, Route
|
from starlette.routing import Mount, Route
|
||||||
|
|
||||||
@@ -534,10 +535,13 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
if oauth_enabled:
|
if oauth_enabled:
|
||||||
|
|
||||||
def oauth_protected_resource_metadata(request):
|
def oauth_protected_resource_metadata(request):
|
||||||
"""RFC 8959 Protected Resource Metadata endpoint."""
|
"""RFC 9728 Protected Resource Metadata endpoint."""
|
||||||
mcp_server_url = os.getenv(
|
mcp_server_url = os.getenv(
|
||||||
"NEXTCLOUD_MCP_SERVER_URL", "http://localhost:8000"
|
"NEXTCLOUD_MCP_SERVER_URL", "http://localhost:8000"
|
||||||
)
|
)
|
||||||
|
# Append /mcp to match the actual resource path (FastMCP streamable-http endpoint)
|
||||||
|
resource_url = f"{mcp_server_url}/mcp"
|
||||||
|
|
||||||
# Use PUBLIC_ISSUER_URL for authorization server since external clients
|
# Use PUBLIC_ISSUER_URL for authorization server since external clients
|
||||||
# (like Claude) need the publicly accessible URL, not internal Docker URLs
|
# (like Claude) need the publicly accessible URL, not internal Docker URLs
|
||||||
public_issuer_url = os.getenv("NEXTCLOUD_PUBLIC_ISSUER_URL")
|
public_issuer_url = os.getenv("NEXTCLOUD_PUBLIC_ISSUER_URL")
|
||||||
@@ -547,14 +551,24 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{
|
{
|
||||||
"resource": mcp_server_url,
|
"resource": resource_url,
|
||||||
"scopes_supported": ["nc:read", "nc:write"],
|
"scopes_supported": ["openid", "nc:read", "nc:write"],
|
||||||
"authorization_servers": [public_issuer_url],
|
"authorization_servers": [public_issuer_url],
|
||||||
"bearer_methods_supported": ["header"],
|
"bearer_methods_supported": ["header"],
|
||||||
"resource_signing_alg_values_supported": ["RS256"],
|
"resource_signing_alg_values_supported": ["RS256"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register PRM endpoint at both path-based and root locations per RFC 9728
|
||||||
|
# Path-based discovery: /.well-known/oauth-protected-resource{path}
|
||||||
|
routes.append(
|
||||||
|
Route(
|
||||||
|
"/.well-known/oauth-protected-resource/mcp",
|
||||||
|
oauth_protected_resource_metadata,
|
||||||
|
methods=["GET"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Root discovery (fallback): /.well-known/oauth-protected-resource
|
||||||
routes.append(
|
routes.append(
|
||||||
Route(
|
Route(
|
||||||
"/.well-known/oauth-protected-resource",
|
"/.well-known/oauth-protected-resource",
|
||||||
@@ -562,11 +576,23 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.info("Protected Resource Metadata (PRM) endpoint enabled")
|
logger.info(
|
||||||
|
"Protected Resource Metadata (PRM) endpoints enabled (path-based + root)"
|
||||||
|
)
|
||||||
|
|
||||||
routes.append(Mount("/", app=mcp_app))
|
routes.append(Mount("/", app=mcp_app))
|
||||||
app = Starlette(routes=routes, lifespan=lifespan)
|
app = Starlette(routes=routes, lifespan=lifespan)
|
||||||
|
|
||||||
|
# Add CORS middleware to allow browser-based clients like MCP Inspector
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # Allow all origins for development
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
expose_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
# Add exception handler for scope challenges (OAuth mode only)
|
# Add exception handler for scope challenges (OAuth mode only)
|
||||||
if oauth_enabled:
|
if oauth_enabled:
|
||||||
|
|
||||||
@@ -584,7 +610,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
"WWW-Authenticate": (
|
"WWW-Authenticate": (
|
||||||
f'Bearer error="insufficient_scope", '
|
f'Bearer error="insufficient_scope", '
|
||||||
f'scope="{scope_str}", '
|
f'scope="{scope_str}", '
|
||||||
f'resource_metadata="{resource_url}/.well-known/oauth-protected-resource"'
|
f'resource_metadata="{resource_url}/.well-known/oauth-protected-resource/mcp"'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content={
|
content={
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ async def test_prm_endpoint():
|
|||||||
"""Test that the Protected Resource Metadata endpoint returns correct data."""
|
"""Test that the Protected Resource Metadata endpoint returns correct data."""
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
# Test the PRM endpoint directly
|
# Test the PRM endpoint directly (RFC 9728 - path includes /mcp resource)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
"http://localhost:8001/.well-known/oauth-protected-resource"
|
"http://localhost:8001/.well-known/oauth-protected-resource/mcp"
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
prm_data = response.json()
|
prm_data = response.json()
|
||||||
assert prm_data["resource"] == "http://localhost:8001"
|
assert prm_data["resource"] == "http://localhost:8001/mcp"
|
||||||
assert "nc:read" in prm_data["scopes_supported"]
|
assert "nc:read" in prm_data["scopes_supported"]
|
||||||
assert "nc:write" in prm_data["scopes_supported"]
|
assert "nc:write" in prm_data["scopes_supported"]
|
||||||
assert "http://localhost:8080" in prm_data["authorization_servers"]
|
assert "http://localhost:8080" in prm_data["authorization_servers"]
|
||||||
|
|||||||
Vendored
+1
-1
Submodule third_party/oidc updated: f7f80b72d5...59b2dce3de
Reference in New Issue
Block a user