{"record":{"id":"b4dfb34d53152305","repo":"PrefectHQ/fastmcp","slug":"client-id-is-required-for-client-registration","errorCode":null,"errorMessage":"client_id is required for client registration","messagePattern":"client_id is required for client registration","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py","lineNumber":988,"sourceCode":"                allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,\n                allow_unregistered_redirect_uris=True,\n            )\n\n        return None\n\n    @override\n    async def register_client(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Register a client locally\n\n        When a client registers, we create a ProxyDCRClient that is more\n        forgiving about validating redirect URIs, since the DCR client's\n        redirect URI will likely be localhost or unknown to the proxied IDP. The\n        proxied IDP only knows about this server's fixed redirect URI.\n        \"\"\"\n\n        # Create a ProxyDCRClient with configured redirect URI validation\n        if client_info.client_id is None:\n            raise ValueError(\"client_id is required for client registration\")\n\n        # SEP-837: the SDK's RegistrationHandler drops application_type when it\n        # builds this object, so prefer the value the HTTP route recovered from\n        # the raw request body. Fall back to the object's own field for direct\n        # (non-HTTP) callers. Write it back so the DCR response echoes the type.\n        #\n        # The SDK splits the registration *request* model from the registered\n        # *client record*: `OAuthClientMetadata.application_type` defaults to\n        # \"native\", while `OAuthClientInformationFull.application_type` is\n        # `str | None` and defaults to None. Normalize the unset case back to\n        # \"native\" so a client that omits the field gets the RFC 7591 default\n        # recorded explicitly, on both the HTTP and direct-call paths.\n        pending_application_type = _pending_application_type.get()\n        if pending_application_type is not None:\n            client_info.application_type = pending_application_type\n        elif client_info.application_type is None:\n            client_info.application_type = \"native\"\n        application_type = client_info.application_type","sourceCodeStart":970,"sourceCodeEnd":1006,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py#L970-L1006","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the registration flow assigns a client_id before register_client is invoked","If using a custom registration handler, generate and set a client_id (e.g. secrets.token_urlsafe) on the client_info","Check the MCP SDK version for regressions in RegistrationHandler that drop client_id"],"exampleFix":"// before\nclient_info = OAuthClientMetaData(redirect_uris=[uri])  # client_id None\nproxy.register_client(client_info)\n// after\nclient_info = OAuthClientMetaData(client_id=secrets.token_urlsafe(16), redirect_uris=[uri])\nproxy.register_client(client_info)","handlingStrategy":"validation","validationCode":"if client_info.client_id is None:\n    client_info = client_info.model_copy(update={\"client_id\": secrets.token_urlsafe(16)})\nproxy.register_client(client_info)","typeGuard":"def has_client_id(client_info) -> bool:\n    return getattr(client_info, \"client_id\", None) is not None","tryCatchPattern":"try:\n    proxy.register_client(client_info)\nexcept ValueError as e:\n    if \"client_id is required\" in str(e):\n        client_info.client_id = secrets.token_urlsafe(16)\n        proxy.register_client(client_info)\n    else:\n        raise","preventionTips":["Always assign client_id in custom registration handlers before calling register_client","Test DCR flows end-to-end so SDK regressions in RegistrationHandler surface early","Pin and review MCP SDK versions for client-building changes"],"tags":["oauth","dcr","client-registration","validation"],"backgroundTag":"missing-required-field","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}