Setting up an AgentCore Identity credential provider for an MCP server that only supports DCR
Many MCP server only support DCR to obtain the client id and secret required to create an AgentCore Indentity credential provider. This article guides you through the process to follow in order correctly create and use the credential provider.
When you front an MCP server with Amazon Bedrock AgentCore Identity, the OAuth2 credential provider needs a client_id and client_secret issued by the MCP server's identity provider (IdP). For "well-known" IdPs (Auth0, Okta, Cognito, …) you log in to a console, register an application, copy the credentials, and paste them into AgentCore.
But many MCP servers (Figma, GitHub Copilot's MCP, Linear, Atlassian, groundcover, …) only expose Dynamic Client Registration (DCR — RFC 7591). You can't pre-create a client by hand — the IdP generates one only when you POST to its registration endpoint with the exact redirect URI the client will use.
That creates a chicken-and-egg problem with AgentCore Identity:
- The provider's redirect URI (
callbackUrl) is generated by AgentCore when you create the provider. - DCR requires the redirect URI before it issues a
client_id. - AgentCore's
create_oauth2_credential_providerrequires non-emptyclientId/clientSecretupfront.
This article walks through the workaround that works in practice: bootstrap with placeholder credentials, register against the MCP IdP using the AgentCore callback, then update the provider with the real credentials. Code samples are Python with boto3 + requests; the same flow works from any AWS SDK.
TL;DR
- Probe the MCP server unauthenticated → get the authorization-server metadata from the
WWW-Authenticateheader. - Create the AgentCore credential provider with placeholder
clientId/clientSecret. Capture the returnedcallbackUrl. - Register a client at the IdP's
registration_endpointusingcallbackUrlas one of theredirect_uris. - Update the AgentCore provider with the real
client_id/client_secret. - Use
get_resource_oauth2_token(USER_FEDERATION flow) — first call returns an authorization URL, thencomplete_resource_token_authbinds the session, the next call returns the access token.
Why placeholders are needed
The AgentCore Identity API contract is:
ac.create_oauth2_credential_provider( name=..., credentialProviderVendor="CustomOauth2", oauth2ProviderConfigInput={ "customOauth2ProviderConfig": { "clientId": ..., # required, non-empty "clientSecret": ..., # required, non-empty "oauthDiscovery": {"discoveryUrl": ...}, } }, )
The response returns a callbackUrl like:
https://bedrock-agentcore.us-east-1.amazonaws.com/identities/oauth2/callback/<uuid>
That UUID is unique per provider and is part of the URL the IdP will redirect to after the user authorizes. You don't know the UUID until the provider exists, but the IdP won't issue a client_id until you can give it that UUID. Hence the bootstrap with throwaway values.
Step-by-step
0. Discover the authorization server
Most MCP servers follow RFC 9728 (OAuth 2.0 Protected Resource Metadata): an unauthenticated request returns 401 with a WWW-Authenticate: Bearer resource_metadata="..." header.
import requests, json probe = requests.post( SERVER, headers={"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}, data=json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}), ) challenge = probe.headers["WWW-Authenticate"] # Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource", scope="mcp:connect"
Parse the resource_metadata URL out of the header, fetch it, and you get back something like:
{ "resource": "https://mcp.example.com/mcp", "authorization_servers": ["https://api.example.com"], "scopes_supported": ["mcp:connect"] }
Then fetch the auth server metadata. Try oauth-authorization-server first (RFC 8414); fall back to openid-configuration:
auth_meta = requests.get( "https://api.example.com/.well-known/oauth-authorization-server" ).json() # { # "authorization_endpoint": "https://...", # "token_endpoint": "https://...", # "registration_endpoint": "https://...", # <-- DCR support # ... # }
If registration_endpoint is missing, the IdP doesn't support DCR and you'd need to pre-register out-of-band; this article assumes it's present.
1. Create the credential provider with placeholders
import boto3 ac = boto3.client("bedrock-agentcore-control", region_name="us-east-1") create_resp = ac.create_oauth2_credential_provider( name="my-mcp-provider", credentialProviderVendor="CustomOauth2", oauth2ProviderConfigInput={ "customOauth2ProviderConfig": { "clientId": "placeholder", "clientSecret": "placeholder", "oauthDiscovery": { "discoveryUrl": "https://api.example.com/.well-known/oauth-authorization-server" }, } }, ) provider_arn = create_resp["credentialProviderArn"] callback_url = create_resp["callbackUrl"] # the URL we need for DCR
The discovery URL must match what the IdP actually serves. Some IdPs only publish OIDC discovery (
/.well-known/openid-configuration). Use whichever fetches successfully.
2. Register the client at the IdP using the AgentCore callback
reg = requests.post( auth_meta["registration_endpoint"], headers={"content-type": "application/json", "accept": "application/json"}, data=json.dumps({ "redirect_uris": [callback_url], # the AgentCore-issued URL "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "client_name": "AgentCore Notebook Client", "client_uri": "https://example.com", "token_endpoint_auth_method": "client_secret_basic", }), ).json() client_id = reg["client_id"] client_secret = reg["client_secret"]
Two things to watch for:
redirect_urismust contain thecallback_urlexactly (including the UUID). Some IdPs allow extras (you can add alocalhostURL for debugging); some don't.token_endpoint_auth_methodhas to match what AgentCore will actually do at the token endpoint. AgentCore uses HTTP Basic auth (client_secret_basic), so register with that. A few IdPs default toclient_secret_post; if you see "invalid client" errors at token exchange, this is the first place to look.
3. Update the AgentCore provider with the real credentials
ac.update_oauth2_credential_provider( name="my-mcp-provider", credentialProviderVendor="CustomOauth2", oauth2ProviderConfigInput={ "customOauth2ProviderConfig": { "clientId": client_id, "clientSecret": client_secret, "oauthDiscovery": { "discoveryUrl": "https://api.example.com/.well-known/oauth-authorization-server" }, } }, )
The provider is now usable.
Things to consider
- Re-running the registration creates a new client every time. DCR endpoints generally don't dedupe. Either keep the credentials around in a vault, or call
update_oauth2_credential_providerimmediately and discard the previous client. If the IdP returns aregistration_client_uriandregistration_access_token, you can use those to delete or rotate the registration later. callbackUrlis region-specific. The UUID prefix ishttps://bedrock-agentcore.<region>.amazonaws.com/identities/oauth2/callback/.... If you switch regions, you must re-register the client because most IdPs reject mismatched redirect URIs.- The discovery URL is locked in at provider creation time. If the IdP later rotates its issuer or moves the well-known endpoints, you need to update the provider (or recreate it).
- Some IdPs expire DCR-issued clients. Figma's never expire (
client_secret_expires_at: 0); some auth servers expire after 90 days. Plan for re-registration if the spec says so. - AgentCore Identity must be able to reach the IdP. The redirect from the IdP comes back to AgentCore's hosted callback, which then calls the IdP's token endpoint server-to-server.
- Audience mismatches. Some IdPs (Auth0 in particular) require the
audienceparameter on the authorize/token requests, and the access token will only be accepted by the gateway if itsaudclaim matches the gateway'sallowedAudienceconfiguration. AgentCore passes acustomParameters={"audience": ...}argument onget_resource_oauth2_tokenfor this case. - Locked-down DCR. Figma, for instance, only accepts registrations from "known" client signatures (VS Code, Kiro, …)
Reference flow as a checklist
requests.post(MCP_SERVER, ...)→ 401 → grabWWW-Authenticate.requests.get(resource_metadata_url)→authorization_servers[0].requests.get(auth_server_base + "/.well-known/oauth-authorization-server")→auth_meta.ac.create_oauth2_credential_provider(... clientId="placeholder" ...)→ savecallbackUrl.requests.post(auth_meta["registration_endpoint"], json={"redirect_uris": [callbackUrl], ...})→ saveclient_id/client_secret.ac.update_oauth2_credential_provider(... real clientId/Secret ...).
Steps 4–6 are the DCR-specific dance.
See also
- Language
- English
Relevant content
asked 10 months ago
asked 10 months ago
