diff --git a/docs/integrations/scalekit.mdx b/docs/integrations/scalekit.mdx
index 89de64ef1..802fe16ef 100644
--- a/docs/integrations/scalekit.mdx
+++ b/docs/integrations/scalekit.mdx
@@ -8,19 +8,16 @@ tag: NEW
import { VersionBadge } from "/snippets/version-badge.mdx"
-
-
+
Install auth stack to your FastMCP server with [Scalekit](https://scalekit.com) using the [Remote OAuth](/servers/auth/remote-oauth) pattern: Scalekit handles user authentication, and the MCP server validates issued tokens.
-## Configuration
-
### Prerequisites
Before you begin
-1. Get a [Scalekit account](https://app.scalekit.com/) and grab API credentials such as **Client ID**, **Client Secret** and **Environment URL** from _Dashboard > Developers > Settings_.
-2. Have your FastMCP server's endpoint ready (can be localhost for development, e.g., `http://localhost:8000/mcp`)
+1. Get a [Scalekit account](https://app.scalekit.com/) and grab your **Environment URL** from _Dashboard > Settings_ .
+2. Have your FastMCP server's base URL ready (can be localhost for development, e.g., `http://localhost:8000/`)
### Step 1: Configure MCP server in Scalekit environment
@@ -36,9 +33,10 @@ In your FastMCP project's `.env`:
```sh
SCALEKIT_ENVIRONMENT_URL=
-SCALEKIT_CLIENT_ID= # skc_7008EXAMPLE46
SCALEKIT_RESOURCE_ID= # res_926EXAMPLE5878
-MCP_URL=http://localhost:8000/mcp
+BASE_URL=http://localhost:8000/
+# Optional: additional scopes tokens must have
+# SCALEKIT_REQUIRED_SCOPES=read,write
```
@@ -48,6 +46,8 @@ MCP_URL=http://localhost:8000/mcp
Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:
+> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.
+
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
@@ -55,9 +55,9 @@ from fastmcp.server.auth.providers.scalekit import ScalekitProvider
# Discovers Scalekit endpoints and set up JWT token validation
auth_provider = ScalekitProvider(
environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
- client_id=SCALEKIT_CLIENT_ID, # OAuth client ID
resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
- mcp_url=SERVER_URL, # Is also aud claim
+ base_url=SERVER_URL, # Public MCP endpoint
+ required_scopes=["read"], # Optional scope enforcement
)
# Create FastMCP server with auth
@@ -75,6 +75,10 @@ def auth_status() -> dict:
```
+
+Set `required_scopes` when you need tokens to carry specific permissions. Leave it unset to allow any token issued for the resource.
+
+
## Testing
### Start the MCP server
@@ -104,16 +108,18 @@ These environment variables provide default values for the Scalekit provider, wh
Your Scalekit environment URL from the Admin Portal (e.g., `https://your-env.scalekit.com`)
-
-Your Scalekit OAuth application client ID from the Applications section
-
-
-Your Scalekit resource server ID from the Resources section
+Your Scalekit resource server ID from the MCP Servers section
-
-Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000/mcp` for development)
+
+Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000/` for development)
+
+
+Legacy `FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL` is still recognized for backward compatibility but will be removed soon-rename it to `...BASE_URL`.
+
+
+Comma-, space-, or JSON-separated list of scopes that tokens must include to access your server
@@ -125,9 +131,10 @@ FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.scalekit.ScalekitProvider
# Scalekit configuration
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL=https://your-env.scalekit.com
-FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_CLIENT_ID=skc_123
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID=res_456
-FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL=https://your-server.com/mcp
+FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL=https://your-server.com/
+# FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES=read,write
+# FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL=https://your-server.com/ # Deprecated
```
With environment variables set, your server code simplifies to:
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
index 7dd465a29..546b07b06 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
@@ -37,9 +37,9 @@ IMPORTANT SETUP REQUIREMENTS:
2. Environment Configuration:
- Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
- - Set SCALEKIT_CLIENT_ID from your OAuth application
- Set SCALEKIT_RESOURCE_ID from your created resource
- - Set MCP_URL to your FastMCP server's public URL
+ - Set BASE_URL to your FastMCP server's public URL
+ - (Optional) Set SCALEKIT_REQUIRED_SCOPES to enforce token scopes
For detailed setup instructions, see:
https://docs.scalekit.com/mcp/overview/
@@ -61,4 +61,3 @@ metadata endpoint that forwards Scalekit's OAuth metadata to clients.
**Args:**
- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
This is used to advertise the resource URL in metadata.
-
diff --git a/examples/auth/scalekit_oauth/README.md b/examples/auth/scalekit_oauth/README.md
index 63c555244..c241d76f7 100644
--- a/examples/auth/scalekit_oauth/README.md
+++ b/examples/auth/scalekit_oauth/README.md
@@ -9,8 +9,8 @@ Demonstrates FastMCP server protection with Scalekit OAuth.
**Create a Scalekit Account**:
- Go to [Scalekit Dashboard](https://app.scalekit.com/)
-- Navigate to **Developers** → **Settings**
-- Copy your Environment URL, Client ID, and Client Secret
+- Copy your Environment URL from **Developers** → **Settings**
+- Copy Resource ID (res_xxx) from **Developers** → **MCP Servers**
**Register Your MCP Server**:
@@ -23,9 +23,10 @@ Create a `.env` file:
```bash
# Required Scalekit credentials
SCALEKIT_ENVIRONMENT_URL=
-SCALEKIT_CLIENT_ID= # skc_7008EXAMPLE46
SCALEKIT_RESOURCE_ID= # res_926EXAMPLE5878
-MCP_URL=http://localhost:8000/mcp
+BASE_URL=http://localhost:8000/
+# Optional: additional scopes tokens must include (comma-separated)
+# SCALEKIT_REQUIRED_SCOPES=read,write
```
### 2. Run the Example
diff --git a/examples/auth/scalekit_oauth/server.py b/examples/auth/scalekit_oauth/server.py
index 40da711e2..68cef23b5 100644
--- a/examples/auth/scalekit_oauth/server.py
+++ b/examples/auth/scalekit_oauth/server.py
@@ -4,9 +4,12 @@ This example demonstrates how to protect a FastMCP server with Scalekit OAuth.
Required environment variables:
- SCALEKIT_ENVIRONMENT_URL: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
-- SCALEKIT_CLIENT_ID: Your Scalekit OAuth application client ID
- SCALEKIT_RESOURCE_ID: Your Scalekit resource ID
+Optional:
+- SCALEKIT_REQUIRED_SCOPES: Comma-separated scopes tokens must include
+- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)
+
To run:
python server.py
"""
@@ -16,12 +19,19 @@ import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+required_scopes_env = os.getenv("SCALEKIT_REQUIRED_SCOPES")
+required_scopes = (
+ [scope.strip() for scope in required_scopes_env.split(",") if scope.strip()]
+ if required_scopes_env
+ else None
+)
+
auth = ScalekitProvider(
environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL")
or "https://your-env.scalekit.com",
- client_id=os.getenv("SCALEKIT_CLIENT_ID") or "",
resource_id=os.getenv("SCALEKIT_RESOURCE_ID") or "",
- mcp_url=os.getenv("MCP_URL", "http://localhost:8000/mcp"),
+ base_url=os.getenv("BASE_URL", "http://localhost:8000/"),
+ required_scopes=required_scopes,
)
mcp = FastMCP("Scalekit OAuth Example Server", auth=auth)
diff --git a/src/fastmcp/server/auth/providers/scalekit.py b/src/fastmcp/server/auth/providers/scalekit.py
index 1aefaf052..6f115b867 100644
--- a/src/fastmcp/server/auth/providers/scalekit.py
+++ b/src/fastmcp/server/auth/providers/scalekit.py
@@ -8,7 +8,7 @@ authentication for seamless MCP client authentication.
from __future__ import annotations
import httpx
-from pydantic import AnyHttpUrl
+from pydantic import AnyHttpUrl, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.responses import JSONResponse
from starlette.routing import Route
@@ -16,6 +16,7 @@ from starlette.routing import Route
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.settings import ENV_FILE
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -30,9 +31,25 @@ class ScalekitProviderSettings(BaseSettings):
)
environment_url: AnyHttpUrl
- client_id: str
resource_id: str
- mcp_url: AnyHttpUrl
+ base_url: AnyHttpUrl | None = None
+ mcp_url: AnyHttpUrl | None = None
+ required_scopes: list[str] | None = None
+
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, value: object):
+ return parse_scopes(value)
+
+ @model_validator(mode="after")
+ def _resolve_base_url(self):
+ resolved = self.base_url or self.mcp_url
+ if resolved is None:
+ msg = "Either base_url or mcp_url must be provided for ScalekitProvider"
+ raise ValueError(msg)
+
+ object.__setattr__(self, "base_url", resolved)
+ return self
class ScalekitProvider(RemoteAuthProvider):
@@ -53,9 +70,8 @@ class ScalekitProvider(RemoteAuthProvider):
2. Environment Configuration:
- Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
- - Set SCALEKIT_CLIENT_ID from your OAuth application
- Set SCALEKIT_RESOURCE_ID from your created resource
- - Set MCP_URL to your FastMCP server's public URL
+ - Set BASE_URL to your FastMCP server's public URL
For detailed setup instructions, see:
https://docs.scalekit.com/mcp/overview/
@@ -67,9 +83,8 @@ class ScalekitProvider(RemoteAuthProvider):
# Create Scalekit resource server provider
scalekit_auth = ScalekitProvider(
environment_url="https://your-env.scalekit.com",
- client_id="sk_client_...",
resource_id="sk_resource_...",
- mcp_url="https://your-fastmcp-server.com",
+ base_url="https://your-fastmcp-server.com",
)
# Use with FastMCP
@@ -83,44 +98,77 @@ class ScalekitProvider(RemoteAuthProvider):
environment_url: AnyHttpUrl | str | NotSetT = NotSet,
client_id: str | NotSetT = NotSet,
resource_id: str | NotSetT = NotSet,
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
mcp_url: AnyHttpUrl | str | NotSetT = NotSet,
+ required_scopes: list[str] | NotSetT = NotSet,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Scalekit resource server provider.
Args:
environment_url: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
- client_id: Your Scalekit OAuth client ID
resource_id: Your Scalekit resource ID
- mcp_url: Public URL of this FastMCP server (used as audience)
+ base_url: Public URL of this FastMCP server
+ required_scopes: Optional list of scopes that must be present in tokens
token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
"""
+ legacy_client_id = client_id is not NotSet
+
settings = ScalekitProviderSettings.model_validate(
{
k: v
for k, v in {
"environment_url": environment_url,
- "client_id": client_id,
"resource_id": resource_id,
+ "base_url": base_url,
"mcp_url": mcp_url,
+ "required_scopes": required_scopes,
}.items()
if v is not NotSet
}
)
+ if settings.mcp_url is not None:
+ logger.warning(
+ "ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. "
+ "Rename it to 'base_url'."
+ )
+
+ if legacy_client_id:
+ logger.warning(
+ "ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward "
+ "compatibility and will be removed in a future release."
+ )
+
self.environment_url = str(settings.environment_url).rstrip("/")
- self.client_id = settings.client_id
self.resource_id = settings.resource_id
- self.mcp_url = str(settings.mcp_url)
+ self.required_scopes = settings.required_scopes or []
+ base_url_value = str(settings.base_url)
+
+ logger.debug(
+ "Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s",
+ self.environment_url,
+ self.resource_id,
+ base_url_value,
+ self.required_scopes,
+ )
# Create default JWT verifier if none provided
if token_verifier is None:
+ logger.debug(
+ "Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
+ f"{self.environment_url}/keys",
+ self.environment_url,
+ self.required_scopes,
+ )
token_verifier = JWTVerifier(
jwks_uri=f"{self.environment_url}/keys",
issuer=self.environment_url,
algorithm="RS256",
- audience=self.mcp_url,
+ required_scopes=self.required_scopes or None,
)
+ else:
+ logger.debug("Using custom token verifier for ScalekitProvider")
# Initialize RemoteAuthProvider with Scalekit as the authorization server
super().__init__(
@@ -128,7 +176,7 @@ class ScalekitProvider(RemoteAuthProvider):
authorization_servers=[
AnyHttpUrl(f"{self.environment_url}/resources/{self.resource_id}")
],
- base_url=self.mcp_url,
+ base_url=base_url_value,
)
def get_routes(
@@ -146,16 +194,27 @@ class ScalekitProvider(RemoteAuthProvider):
"""
# Get the standard protected resource routes from RemoteAuthProvider
routes = super().get_routes(mcp_path)
+ logger.debug(
+ "Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s",
+ mcp_path,
+ self.resource_id,
+ )
async def oauth_authorization_server_metadata(request):
"""Forward Scalekit OAuth authorization server metadata with FastMCP customizations."""
try:
+ metadata_url = f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
+ logger.debug(
+ "Fetching Scalekit OAuth metadata: metadata_url=%s", metadata_url
+ )
async with httpx.AsyncClient() as client:
- response = await client.get(
- f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
- )
+ response = await client.get(metadata_url)
response.raise_for_status()
metadata = response.json()
+ logger.debug(
+ "Scalekit metadata fetched successfully: metadata_keys=%s",
+ list(metadata.keys()),
+ )
return JSONResponse(metadata)
except Exception as e:
logger.error(f"Failed to fetch Scalekit metadata: {e}")
diff --git a/tests/server/auth/providers/test_scalekit.py b/tests/server/auth/providers/test_scalekit.py
index f33ca5952..70470a149 100644
--- a/tests/server/auth/providers/test_scalekit.py
+++ b/tests/server/auth/providers/test_scalekit.py
@@ -19,15 +19,36 @@ class TestScalekitProvider:
"""Test ScalekitProvider initialization with explicit parameters."""
provider = ScalekitProvider(
environment_url="https://my-env.scalekit.com",
- client_id="sk_client_123",
resource_id="sk_resource_456",
- mcp_url="https://myserver.com/",
+ base_url="https://myserver.com/",
+ required_scopes=["read"],
)
assert provider.environment_url == "https://my-env.scalekit.com"
- assert provider.client_id == "sk_client_123"
assert provider.resource_id == "sk_resource_456"
- assert str(provider.mcp_url) == "https://myserver.com/"
+ assert str(provider.base_url) == "https://myserver.com/"
+ assert provider.required_scopes == ["read"]
+
+ def test_init_with_mcp_url_only(self):
+ """Allow legacy mcp_url parameter as base_url."""
+ provider = ScalekitProvider(
+ environment_url="https://legacy.scalekit.com",
+ resource_id="sk_resource_legacy",
+ mcp_url="https://legacy-app.com/",
+ )
+
+ assert str(provider.base_url) == "https://legacy-app.com/"
+
+ def test_init_prefers_base_url_over_mcp_url(self):
+ """mcp_url should take precedence over base_url when both provided."""
+ provider = ScalekitProvider(
+ environment_url="https://my-env.scalekit.com",
+ resource_id="sk_resource_456",
+ base_url="https://preferred-base.com/",
+ mcp_url="https://unused-base.com/",
+ )
+
+ assert str(provider.base_url) == "https://preferred-base.com/"
def test_init_with_env_vars(self):
"""Test ScalekitProvider initialization from environment variables."""
@@ -35,51 +56,72 @@ class TestScalekitProvider:
os.environ,
{
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL": "https://env-scalekit.com",
- "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_CLIENT_ID": "skc_123",
"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID": "res_456",
- "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL": "https://envserver.com/mcp",
+ "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL": "https://envserver.com/mcp",
+ "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES": "read,write",
},
):
provider = ScalekitProvider()
assert provider.environment_url == "https://env-scalekit.com"
- assert provider.client_id == "skc_123"
assert provider.resource_id == "res_456"
- assert str(provider.mcp_url) == "https://envserver.com/mcp"
+ assert str(provider.base_url) == "https://envserver.com/mcp"
+ assert provider.required_scopes == ["read", "write"]
+
+ def test_init_with_legacy_env_var(self):
+ """FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL should still be supported."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL": "https://env-scalekit.com",
+ "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID": "res_456",
+ "FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL": "https://legacy-env.com/",
+ },
+ ):
+ provider = ScalekitProvider()
+
+ assert str(provider.base_url) == "https://legacy-env.com/"
def test_environment_variable_loading(self):
"""Test that environment variables are loaded correctly."""
provider = ScalekitProvider(
environment_url="https://test-env.scalekit.com",
- client_id="sk_client_test_123",
resource_id="sk_resource_test_456",
- mcp_url="http://test-server.com",
+ base_url="http://test-server.com",
)
assert provider.environment_url == "https://test-env.scalekit.com"
- assert provider.client_id == "sk_client_test_123"
assert provider.resource_id == "sk_resource_test_456"
- assert str(provider.mcp_url) == "http://test-server.com/"
+ assert str(provider.base_url) == "http://test-server.com/"
+
+ def test_accepts_client_id_argument(self):
+ """client_id parameter should be accepted but ignored."""
+ provider = ScalekitProvider(
+ environment_url="https://my-env.scalekit.com",
+ resource_id="sk_resource_456",
+ base_url="https://myserver.com/",
+ client_id="client_123",
+ )
+
+ assert str(provider.base_url) == "https://myserver.com/"
def test_url_trailing_slash_handling(self):
"""Test that URLs handle trailing slashes correctly."""
provider = ScalekitProvider(
environment_url="https://my-env.scalekit.com/",
- client_id="sk_client_123",
resource_id="sk_resource_456",
- mcp_url="https://myserver.com/",
+ base_url="https://myserver.com/",
)
assert provider.environment_url == "https://my-env.scalekit.com"
- assert str(provider.mcp_url) == "https://myserver.com/"
+ assert str(provider.base_url) == "https://myserver.com/"
def test_jwt_verifier_configured_correctly(self):
"""Test that JWT verifier is configured correctly."""
provider = ScalekitProvider(
environment_url="https://my-env.scalekit.com",
- client_id="sk_client_123",
resource_id="sk_resource_456",
- mcp_url="https://myserver.com/",
+ base_url="https://myserver.com/",
)
# Check that JWT verifier uses the correct endpoints
@@ -90,15 +132,27 @@ class TestScalekitProvider:
assert (
provider.token_verifier.issuer == "https://my-env.scalekit.com" # type: ignore[attr-defined]
)
- assert provider.token_verifier.audience == "https://myserver.com/" # type: ignore[attr-defined]
+ assert (
+ provider.token_verifier.audience is None # type: ignore[attr-defined]
+ )
+
+ def test_required_scopes_hooks_into_verifier(self):
+ """Token verifier should enforce required scopes when provided."""
+ provider = ScalekitProvider(
+ environment_url="https://my-env.scalekit.com",
+ resource_id="sk_resource_456",
+ base_url="https://myserver.com/",
+ required_scopes=["read"],
+ )
+
+ assert provider.token_verifier.required_scopes == ["read"] # type: ignore[attr-defined]
def test_authorization_servers_configuration(self):
"""Test that authorization servers are configured correctly."""
provider = ScalekitProvider(
environment_url="https://my-env.scalekit.com",
- client_id="sk_client_123",
resource_id="sk_resource_456",
- mcp_url="https://myserver.com/",
+ base_url="https://myserver.com/",
)
assert len(provider.authorization_servers) == 1
@@ -114,9 +168,8 @@ async def mcp_server_url():
mcp = FastMCP(
auth=ScalekitProvider(
environment_url="https://test-env.scalekit.com",
- client_id="sk_client_test_123",
resource_id="sk_resource_test_456",
- mcp_url="http://localhost:4321",
+ base_url="http://localhost:4321",
)
)
@@ -147,9 +200,60 @@ class TestScalekitProviderIntegration:
assert exc_info.value.response.status_code == 401
assert "tools" not in locals()
- # async def test_authorized_access(self, client_with_headless_oauth: Client):
- # async with client_with_headless_oauth:
- # tools = await client_with_headless_oauth.list_tools()
- # assert tools is not None
- # assert len(tools) > 0
- # assert "add" in tools
+ async def test_metadata_route_forwards_scalekit_response(
+ self,
+ monkeypatch: pytest.MonkeyPatch,
+ mcp_server_url: str,
+ ) -> None:
+ """Ensure Scalekit metadata route proxies upstream JSON."""
+
+ metadata_payload = {
+ "issuer": "https://test-env.scalekit.com",
+ "token_endpoint": "https://test-env.scalekit.com/token",
+ "authorization_endpoint": "https://test-env.scalekit.com/authorize",
+ }
+
+ class DummyResponse:
+ status_code = 200
+
+ def __init__(self, data: dict[str, str]):
+ self._data = data
+
+ def json(self):
+ return self._data
+
+ def raise_for_status(self):
+ return None
+
+ class DummyAsyncClient:
+ last_url: str | None = None
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb):
+ return False
+
+ async def get(self, url: str):
+ DummyAsyncClient.last_url = url
+ return DummyResponse(metadata_payload)
+
+ real_httpx_client = httpx.AsyncClient
+
+ monkeypatch.setattr(
+ "fastmcp.server.auth.providers.scalekit.httpx.AsyncClient",
+ DummyAsyncClient,
+ )
+
+ base_url = mcp_server_url.rsplit("/mcp", 1)[0]
+ async with real_httpx_client() as client:
+ response = await client.get(
+ f"{base_url}/.well-known/oauth-authorization-server"
+ )
+
+ assert response.status_code == 200
+ assert response.json() == metadata_payload
+ assert (
+ DummyAsyncClient.last_url
+ == "https://test-env.scalekit.com/.well-known/oauth-authorization-server/resources/sk_resource_test_456"
+ )