PrefectHQ/fastmcp · error · ValueError

Either config_url (new API) or both project_id and descope_b

Error message

Either config_url (new API) or both project_id and descope_base_url (old API) must be provided

What it means

DescopeAuthProvider requires either the new API form (config_url) or the old API form (project_id plus descope_base_url). The constructor raises this ValueError when neither form is satisfied — typically when only project_id is given without a base URL, or a base URL without project_id, or neither argument at all.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/providers/descope.py:171

        )

        if config_url is not None:
            (
                self.descope_base_url,
                self.project_id,
                issuer_url,
                self.openid_configuration_url,
            ) = _parse_descope_config_url(str(config_url))
        elif project_id is not None and descope_base_url is not None:
            self.project_id = project_id
            descope_base_url_str = str(descope_base_url).rstrip("/")
            if not descope_base_url_str.startswith(("http://", "https://")):
                descope_base_url_str = f"https://{descope_base_url_str}"
            self.descope_base_url = descope_base_url_str
            issuer_url = f"{self.descope_base_url}/v1/apps/{self.project_id}"
            self.openid_configuration_url = f"{issuer_url}{_OPENID_WK}"
        else:
            raise ValueError(
                "Either config_url (new API) or both project_id and descope_base_url (old API) must be provided"
            )

        self.oauth_authorization_server_metadata_url = (
            self.openid_configuration_url.replace(_OPENID_WK, _OAUTH_WK)
        )

        # Advertised scopes are discovered from Descope's OpenID configuration
        # only when the caller supplied neither explicit advertised scopes nor
        # required scopes. Discovery is deferred to the first protected resource
        # metadata request (see get_routes) so construction never performs I/O
        # and a transient failure can be retried instead of being frozen for the
        # provider's lifetime.
        custom_verifier_scopes = (
            token_verifier.scopes_supported if token_verifier is not None else []
        )
        self._scopes_discovery_enabled = (
            parsed_scopes_supported is None

View on GitHub (pinned to 1f02114297)

Solutions

  1. Pass config_url pointing to Descope's OIDC configuration (new API), e.g. config_url=f"https://api.descope.com/v1/apps/<project_id>/.well-known/openid-configuration"
  2. Alternatively pass BOTH project_id and descope_base_url (e.g. project_id='P2abc...', descope_base_url='https://api.descope.com')
  3. Verify keyword argument names and that the values are non-empty strings, not None from failed env lookups

Example fix

// before
auth = DescopeAuthProvider(project_id=os.getenv("DESCOPE_PROJECT_ID"))
// after
auth = DescopeAuthProvider(
    project_id=os.environ["DESCOPE_PROJECT_ID"],
    descope_base_url="https://api.descope.com",
)
# or the new API:
auth = DescopeAuthProvider(
    config_url="https://api.descope.com/v1/apps/P2abc/.well-known/openid-configuration"
)
Defensive patterns

Strategy: validation

Validate before calling

config_url = os.getenv("DESCOPE_CONFIG_URL")
project_id = os.getenv("DESCOPE_PROJECT_ID")
base_url = os.getenv("DESCOPE_BASE_URL")
if not config_url and not (project_id and base_url):
    raise ValueError("Provide either DESCOPE_CONFIG_URL or both DESCOPE_PROJECT_ID and DESCOPE_BASE_URL")
provider = DescopeAuthProvider(config_url=config_url) if config_url else DescopeAuthProvider(project_id=project_id, descope_base_url=base_url)

Type guard

def has_descope_args(config_url: str | None, project_id: str | None, base_url: str | None) -> bool:
    return bool(config_url) or bool(project_id and base_url)

Try / catch

try:
    provider = DescopeAuthProvider(project_id=pid, descope_base_url=base)
except ValueError as e:
    logger.error("Descope auth misconfigured: %s", e)
    raise SystemExit(1) from e

Prevention

When it happens

Trigger: Calling DescopeAuthProvider(...) with: (1) no arguments, (2) only project_id but no descope_base_url, (3) only descope_base_url but no project_id, (4) both but one is None/empty so the `if` branch fails.

Common situations: Migrating from the old Descope setup to the new config_url-based setup and removing old kwargs piecemeal; copying example code that assumes env vars supply project_id/base_url; typos in keyword names (e.g. base_url instead of descope_base_url) silently leaving them unset.

Understand the failure class

Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/101f7f29519ddb42. Report an issue: GitHub.