{"slug": "mcp-authorization-with-dynamic-client-registration", "title": "MCP Authorization with Dynamic Client Registration", "summary": "Christian Posta published a bonus blog post implementing MCP Authorization with Dynamic Client Registration, building an MCP client that follows RFC 7591 to automatically discover and register OAuth clients with MCP servers. The implementation enables plug-and-play interoperability between MCP clients and servers by handling HTTP 401 responses and parsing WWW-Authenticate headers.", "body_md": "# MCP Authorization With Dynamic Client Registration\n\nThis is a bonus post following on from my [Understanding MCP Authorization] three part series covering building (and understanding) an MCP HTTP based server and implementing the MCP Authorization spec [(2025-06-18)](https://modelcontextprotocol.io/specification/2025-06-18/changelog). In the previous series, we built the server side of the spec, leaving the client side up to the reader since obtaining OAuth clients is usually fairly opinionated in enterprise environments.\n\nThe MCP Authorization spec actually [has opinions](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration) about how MCP clients (and thus, OAuth clients) should be created. The idea behind the spec is to allow MCP clients and servers [to be “plug and play” automatically](https://aws.amazon.com/blogs/opensource/open-protocols-for-agent-interoperability-part-2-authentication-on-mcp/). That is, allow any MCP client to automatically discover what it needs to connect to an MCP server.\n\nIn this blog post, we implement an MCP client with Dynamic Client Registration for the OAuth client.\n\nThis series of blog posts (three parts + [source code](https://github.com/christian-posta/mcp-auth-step-by-step)), walks “step-by-step” through the latest MCP Authorization spec and implement it. I have made all of the [source code for each of the steps available on GitHub](https://github.com/christian-posta/mcp-auth-step-by-step).\n\n- Part 1:\n[Implement a spec compliant remote MCP server with HTTP Transport](https://blog.christianposta.com/understanding-mcp-authorization-step-by-step/) - Part 2:\n[Layer in Authorization specification with OAuth 2.1](https://blog.christianposta.com/understanding-mcp-authorization-step-by-step-part-two/) - Part 3:\n[Bring in a production Identity Provider (Keycloak)](https://blog.christianposta.com/understanding-mcp-authorization-step-by-step-part-three/)\n\nFollow ([@christianposta](https://x.com/christianposta) or [/in/ceposta](https://linkedin.com/in/ceposta)) for the next parts.\n\n## Building an MCP Client\n\nFollow along with the source code [for this step](https://github.com/christian-posta/mcp-auth-step-by-step/blob/main/src/mcp_http/step11.py).\n\nThe MCP client we build for this blog will focus on Dynamic Client Registration following [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). The process starts when an MCP client makes a request for a resource it is not authenticated for (HTTP 401). In that case, the MCP server would return a header `WWW-Authenticate`\n\nto help the MCP client figure out how to authenticate.\n\nFrom the spec:\n\nMCP servers MUST use the HTTP header WWW-Authenticate when returning a 401 Unauthorized to indicate the location of the resource server metadata URL as described in RFC9728 Section 5.1 “WWW-Authenticate Response”.\n\nMCP clients MUST be able to parse WWW-Authenticate headers and respond appropriately to HTTP 401 Unauthorized responses from the MCP server.\n\nIn our implementation in [step10](https://github.com/christian-posta/mcp-auth-step-by-step/blob/main/src/mcp_http/step11.py), we built the MCP server to return a 401 and Header:\n\n```\n1\nWWW-Authenticate: Bearer realm=\"mcp-server\", resource_metadata=\"http://localhost:9000/.well-known/oauth-protected-resource\"\n```\n\nNote this part in the value of the header: *resource_metadata=”http://localhost:9000/.well-known/oauth-protected-resource*.\n\nThe MCP client will then request the `oauth-protected-resource`\n\nmetadata (following [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)).\n\nIn our MCP server, it looks like this:\n\n```\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n{\n  \"resource\": \"http://localhost:9000\",\n  \"authorization_servers\": [\"http://localhost:8080/realms/mcp-realm\"],\n  \"scopes_supported\": [\n    \"echo-mcp-server-audience\",\n    \"mcp:read\",\n    \"mcp:tools\",\n    \"mcp:prompts\"\n  ],\n  \"bearer_methods_supported\": [\"header\"],\n  \"resource_documentation\": \"http://localhost:9000/docs\",\n  \"mcp_protocol_version\": \"2025-06-18\",\n  \"resource_type\": \"mcp-server\"\n}\n```\n\nA number of interesting points here. The `authorization_servers`\n\nis where the client should look for how to connect to the Authorization Server (AS). The `scopes_supported`\n\nis how the MCP server tells its clients what scopes it will need to make calls. Interestingly this will be ALL of the scopes needed to access all parts of the MCP server. When the MCP client registers an OAuth client, it should use these scopes. It’s on the AS to determine which users have which roles and what scopes will actually appear in their tokens. When the MCP client initiates an authorization code flow, it should use these scopes.\n\nThe next step is to call the Authorization Server’s (AS) metadata resource based on what was in the `authorization_servers`\n\nlist. The MCP client will append `/.well-known/oauth-authorization-server`\n\nto the value of `authorization_servers`\n\nto discover the endpoints for authorization, client registration, etc on the Authorization Server (AS). In this case, the metadata is located here:\n\n```\n1\nhttp://localhost:8080/realms/mcp-realm/.well-known/oauth-authorization-server\n```\n\nIn our example, since we are using Keycloak, you’ll note the `/realms/<realm-name>`\n\nhere. Other Authorization Servers will be different. When our MCP client calls this and parses the AS metadata, the MCP client should now have enough information to initiate a Dynamic Client Registration and proceed to OAuth flows:\n\nThe relevant endpoints in our example:\n\n```\n1\n2\n3\ntoken_endpoint: http://localhost:8080/realms/mcp-realm/protocol/openid-connect/token\nauthorization_endpoint: http://localhost:8080/realms/mcp-realm/protocol/openid-connect/auth\nregistration_endpoint: http://localhost:8080/realms/mcp-realm/clients-registrations/openid-connect\n```\n\nBased on that response, we see that the `registration_endpoint`\n\nis: `http://localhost:8080/realms/mcp-realm/clients-registrations/openid-connect`\n\n. Now our MCP client should make a call to the `http://localhost:8080/realms/mcp-realm/clients-registrations/openid-connect`\n\nendpoint to register the OAuth client. Here’s an example of what that payload should look like:\n\n```\n1\n2\n3\n4\n5\n6\n7\n{\n  \"client_name\": \"My Anonymous Client\",\n  \"redirect_uris\": [\"http://localhost:9090/callback\"],\n  \"grant_types\": [\"authorization_code\"],\n  \"scope\": \"mcp:read mcp:tools mcp:prompts echo-mcp-server-audience\",\n  \"token_endpoint_auth_method\": \"client_secret_basic\"\n}\n```\n\nIt’s important to know that the MCP Authorization spec (at the time of this writing) expects the AS to allow anonymous OAuth client registration (more on this later). The `redirect_uris`\n\nand `scope`\n\nparameters here are important. The scopes we specify here should match what the MCP server publishes in `/.well-known/oauth-protected-resource`\n\n. Additionally, the MCP client will need to be able to get the authorization code from the OAuth server, so registering the callback is critical.\n\nIMPORTANT: note, that before this call can succeed, we will need to allow anonymous client registration in Keycloak. This is disabled by default (for good reason), so to follow along in the MCP Authorization spec, we’ll need to relax this and enable a client to register anonymously. Follow your organization’s security best practices before considering this. See **Appendix A** of this blog to see how to enable anonymous client registration for the purposes of illustration in this blog.\n\nThis will create a new, dynamic client in Keycloak (or associated IdP). See previous blog post [for setting up Keycloak](https://blog.christianposta.com/understanding-mcp-authorization-step-by-step-part-three/).\n\nYou can see the new OAuth client in the client list above. The MCP client should save the OAuth `client_id`\n\n(and any client credentials). Now that we have an OAuth client, we can proceed to call the `authorization_endpoint`\n\nwhich is `http://localhost:8080/realms/mcp-realm/protocol/openid-connect/auth`\n\nfrom our previously discovered URLs. For example, to get the consent from the user, we send the user to the following URL:\n\n```\n1\nhttp://localhost:8080/realms/mcp-realm/protocol/openid-connect/auth?response_type=code&client_id=f212a20e-8556-4376-b1c9-a28dd4adb2ea&redirect_uri=http%3A%2F%2Flocalhost%3A9090%2Fcallback&scope=mcp%3Aread+mcp%3Atools+mcp%3Aprompts+echo-mcp-server-audience&state=FTqz4qpaoowKk-LeH-HTNA&resource=http%3A%2F%2Flocalhost%3A9000\n```\n\nNOTE: We use the `resource`\n\nparameter here, but Keycloak does not respect RFC 8707. We add it here since that is suggested by the MCP Authorization spec.\n\nThis takes us to the Keycloak authentication page. We can sign in with one of our previously registered users:\n\n```\n1\n2\nusername=mcp-user\npassword=user123\n```\n\nIf authentication is successful, Keycloak will show you the consent page which displays the scopes requested by the client and asks you to grant these authorizations (ie, delegate these authorizations on your behalf).\n\nThis will redirect back to our MCP client with an authorization code. The client can now use this code to request an access token. The client should use PKCE to prevent stolen authorization codes from giving attackers access to get access tokens.\n\n```\n1\nhttp://localhost:9090/callback?state=FTqz4qpaoowKk-LeH-HTNA&session_state=1d6a90eb-0409-4452-b976-431abd602f09&iss=http%3A%2F%2Flocalhost%3A8080%2Frealms%2Fmcp-realm&code=e7560da7-d904-4ef0-ab02-c1d6aac7eec3.1d6a90eb-0409-4452-b976-431abd602f09.f212a20e-8556-4376-b1c9-a28dd4adb2ea\n```\n\nAt this point, we should now be authenticated, and our OAuth client can see the MCP tools list:\n\n```\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n{                                    \n  \"jsonrpc\": \"2.0\",                  \n  \"id\": 1,                           \n  \"result\": {                        \n    \"tools\": [                       \n      {                              \n        \"name\": \"echo\",              \n        \"title\": \"Echo Tool\",                                              \n        \"description\": \"Echo a message\",                                   \n        \"inputSchema\": {                                                   \n          \"properties\": {                                                  \n            \"message\": {                                                   \n              \"description\": \"Message to echo\",                            \n              \"title\": \"Message\",                                          \n              \"type\": \"string\"                                             \n            },                       \n            \"repeat_count\": {                                              \n              \"default\": 1,                                                \n              \"maximum\": 10,                                               \n              \"minimum\": 1,                                                \n              \"title\": \"Repeat Count\",                                     \n              \"type\": \"integer\"                                            \n            }                        \n          },                         \n          \"required\": [              \n            \"message\"                \n          ],                         \n          \"title\": \"EchoRequest\",                                          \n          \"type\": \"object\"                                                 \n        },                           \n        \"outputSchema\": {                                                  \n          \"type\": \"object\",                                                \n          \"properties\": {                                                  \n            \"text\": {                \n              \"type\": \"string\",                                            \n              \"description\": \"The echoed message\"                                                                                                     \n            }                        \n          }                          \n        },                           \n        \"annotations\": {                                                   \n          \"title\": \"Echo Tool\",                                            \n          \"readOnlyHint\": false,                                           \n          \"destructiveHint\": false,                                        \n          \"idempotentHint\": true,                                          \n          \"openWorldHint\": false                                           \n        },                           \n        \"meta\": null                 \n      }                              \n    ]                                \n  }                                  \n}\n```\n\n## Using MCP Inspector\n\nWe have created an OAuth our own custom MCP client to illustrate what an MCP client should do and how it can dynamically connect up to an MCP server it may not have known about in advance. This achieves the plug and play goal of MCP. We will also show how to do this in the [mcp-inspector] tool for testing MCP servers.\n\nNOTE: At the time of writing, there are [some bugs](https://github.com/modelcontextprotocol/inspector/issues/587) in the inspector tool. It passes the scopes (all of the scopes) from the authorization server metadata when it registers an MCP client. It should not do this (least privilege, etc). For now I am using my [patched version here](https://github.com/christian-posta/mcp-inspector/tree/ceposta-patches).\n\nNOTE: At the time of this writing, Keycloak does not handle CORS well on the AS metadata. We will use a reverse proxy (ie, [Agent Gateway](https://agentgateway.dev)) to solve that.\n\nGoing to the MCP inspector page, let’s enter the right transport (HTTP Streamable), URL `http://localhost:9000/mcp`\n\nand then let’s click on the `Open Auth Settings`\n\nso we can watch step by step how the MCP client handles auth.\n\nOnce we click we should see something similar to this:\n\nNow scroll down to see all of the steps we can walk through:\n\nYou can click on the `Continue`\n\nto step through the process of retrieving the metadata, registering the client, and then getting an auth code and token:\n\n#### 1. Discover the Metadata\n\n#### 2. Client Registration\n\n#### 3. Prepare Authorization\n\nYou can click the little arrow (or copy/paste the URL) to start the auth-code flow.\n\n#### 4. Request Authorization Code\n\nIf you login and consent, you should see the auth-code:\n\nAnd you can copy-paste that code into the `mcp-inspector`\n\n:\n\nAnd now you can click continue to finish that step:\n\n#### 5. Token Request / 6. Auth Complete\n\n### Auth Complete\n\nNow that we have an OAuth access token, we can click “Connect” on the left-hand panel, which will send the access token along with the `Initialize`\n\nMCP message. You should see that `mcp-inspector`\n\nconnects successfully.\n\nFrom here, you can list tools, and you can see the only tool we expose on our MCP server, the `echo`\n\ntool:\n\n## Wrapping up\n\nAt this point, we have successfully demonstrating the Dynamic Client Registration part of thee MCP Auth specification. Now, for the astute reader, you’ll notice some areas in this flow that may cause friction in enterprise use cases. In my next blog, I’ll do an encore to my [“The MCP Authorization Spec Is… A Mess for Enterprise”](https://blog.christianposta.com/the-updated-mcp-oauth-spec-is-a-mess/) blog post and dig into why Dynamic Client Registration may not be all that it’s cracked up to be. [Stay tuned](https://linkedin.com/in/ceposta)!!\n\n### Appendix A: Enabling Anonymous Client Registration\n\nThis is not a best practice. We can do this in the sandbox environment for this blog, but for your organization, follow your security best practices.\n\nTo enable anonymous client registration for our blog post, we will need to configure the “Anonymous access policies”, specifically the “Trusted Hosts” and “Allowed Clients Scopes” policy.\n\nFor Trusted Hosts, I will enable my host to be able to call the registration endpoint (check the docker logs to see what the right IP is if it fails):\n\nNOTE: uncheck the “Client URIs must match” checkbox.\n\nLastly, we need to allow anonymous clients to request the right scopes to make this work:\n\n[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)by the author.", "url": "https://wpnews.pro/news/mcp-authorization-with-dynamic-client-registration", "canonical_source": "https://blog.christianposta.com/understanding-mcp-authorization-with-dynamic-client-registration/", "published_at": "2026-06-26 06:41:40+00:00", "updated_at": "2026-06-26 07:04:50.042521+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["Christian Posta", "MCP", "OAuth", "RFC 7591", "Keycloak", "GitHub", "AWS"], "alternates": {"html": "https://wpnews.pro/news/mcp-authorization-with-dynamic-client-registration", "markdown": "https://wpnews.pro/news/mcp-authorization-with-dynamic-client-registration.md", "text": "https://wpnews.pro/news/mcp-authorization-with-dynamic-client-registration.txt", "jsonld": "https://wpnews.pro/news/mcp-authorization-with-dynamic-client-registration.jsonld"}}