PrefectHQ/fastmcp · error · ValueError

client_id is required for client registration

Error message

client_id is required for client registration

What it means

Dynamic client registration must produce a ProxyDCRClient with a client_id; the SDK's RegistrationHandler can build a client_info object with client_id=None in degenerate cases. OAuthProxy.register_client refuses such input with ValueError because a proxy client without an ID cannot participate in the OAuth flow.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py:988

                allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
                allow_unregistered_redirect_uris=True,
            )

        return None

    @override
    async def register_client(self, client_info: OAuthClientInformationFull) -> None:
        """Register a client locally

        When a client registers, we create a ProxyDCRClient that is more
        forgiving about validating redirect URIs, since the DCR client's
        redirect URI will likely be localhost or unknown to the proxied IDP. The
        proxied IDP only knows about this server's fixed redirect URI.
        """

        # Create a ProxyDCRClient with configured redirect URI validation
        if client_info.client_id is None:
            raise ValueError("client_id is required for client registration")

        # SEP-837: the SDK's RegistrationHandler drops application_type when it
        # builds this object, so prefer the value the HTTP route recovered from
        # the raw request body. Fall back to the object's own field for direct
        # (non-HTTP) callers. Write it back so the DCR response echoes the type.
        #
        # The SDK splits the registration *request* model from the registered
        # *client record*: `OAuthClientMetadata.application_type` defaults to
        # "native", while `OAuthClientInformationFull.application_type` is
        # `str | None` and defaults to None. Normalize the unset case back to
        # "native" so a client that omits the field gets the RFC 7591 default
        # recorded explicitly, on both the HTTP and direct-call paths.
        pending_application_type = _pending_application_type.get()
        if pending_application_type is not None:
            client_info.application_type = pending_application_type
        elif client_info.application_type is None:
            client_info.application_type = "native"
        application_type = client_info.application_type

View on GitHub (pinned to 1f02114297)

Solutions

  1. Ensure the registration flow assigns a client_id before register_client is invoked
  2. If using a custom registration handler, generate and set a client_id (e.g. secrets.token_urlsafe) on the client_info
  3. Check the MCP SDK version for regressions in RegistrationHandler that drop client_id

Example fix

// before
client_info = OAuthClientMetaData(redirect_uris=[uri])  # client_id None
proxy.register_client(client_info)
// after
client_info = OAuthClientMetaData(client_id=secrets.token_urlsafe(16), redirect_uris=[uri])
proxy.register_client(client_info)
Defensive patterns

Strategy: validation

Validate before calling

if client_info.client_id is None:
    client_info = client_info.model_copy(update={"client_id": secrets.token_urlsafe(16)})
proxy.register_client(client_info)

Type guard

def has_client_id(client_info) -> bool:
    return getattr(client_info, "client_id", None) is not None

Try / catch

try:
    proxy.register_client(client_info)
except ValueError as e:
    if "client_id is required" in str(e):
        client_info.client_id = secrets.token_urlsafe(16)
        proxy.register_client(client_info)
    else:
        raise

Prevention

When it happens

Trigger: Calling register_client with a client_info whose client_id is None — e.g. via the DCR route (_register_client) or directly from _start_flow with a partially-built ClientRegistration.

Common situations: Custom DCR handlers or middleware constructing OAuthClientMetaData/registration objects manually without assigning an ID; SDK behavior changes in newer MCP versions dropping client_id; test harnesses fabricating client objects.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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