# Connecting Claude Code to a Cognito MCP server — a pre-registered client ID, or headersHelper

> Two ways to connect Claude Code to an MCP server on Bedrock AgentCore Runtime behind Amazon Cognito, which has no dynamic client registration.

- Source: https://oharu121.com/blog/claude-code-cognito-mcp-oauth-client-id-headershelper/
- Published: 2026-10-08T01:22:06+09:00
- Tags: Claude Code, MCP, AgentCore, Amazon Cognito

---
## Introduction

I had an MCP server running on Bedrock AgentCore Runtime, with Amazon Cognito handling inbound auth, and wanted to register it in Claude Code so that `/mcp` → Authenticate would sign me in through the browser.

The plain `claude mcp add` stops before any browser opens, with `Incompatible auth server: does not support dynamic client registration`. Pasting a Cognito access token in as a header connects, until the token expires (one hour by default) and the server comes back as `Token has expired`.

Claude Code has two OAuth routes and two header routes, and **two of the four work against Cognito: a pre-registered app client ID, and `headersHelper`**. The first gives a browser sign-in with token refresh handled by Claude Code; the second mints a fresh token from a script on every connection, so neither needs re-registering when a token runs out.

## Four ways Claude Code authenticates to a remote MCP server

An HTTP MCP server in Claude Code authenticates in one of four ways, and they split by **who obtains the token**. In the two OAuth routes, Claude Code runs the browser flow and stores the token itself. In the two header routes, something outside Claude Code produces the token and Claude Code only forwards it.

*Figure — AuthRoutes: Only the default OAuth route needs a registration endpoint, and that is the one piece Cognito does not publish.*

| Route | Configured with | Browser sign-in | When the token expires | Works with Cognito |
| --- | --- | --- | --- | --- |
| OAuth, dynamic registration | nothing (default) | yes | Claude Code refreshes it | no |
| OAuth, pre-registered client | `oauth.clientId`, `oauth.callbackPort` | yes | Claude Code refreshes it | **yes** |
| Static header | `headers.Authorization` | no | connection fails until you replace it | yes, for one token lifetime |
| `headersHelper` | a command that prints headers | no | the command runs again | **yes** |

The routes do not combine. Claude Code's documentation states that once `headers.Authorization` is set, it "does not fall back to OAuth", so a rejected header is reported as a failed connection rather than as a server needing sign-in.

## Why the default OAuth flow stops at Cognito

**Dynamic client registration** (RFC 7591) lets an OAuth client create its own client ID by posting to the authorization server's `registration_endpoint`. Claude Code uses it by default, which is why a public MCP server needs nothing more than its URL.

Discovery itself works against AgentCore. A request without a token returns a 401 whose `WWW-Authenticate` header points at the runtime's RFC 9728 protected-resource metadata, and that names the Cognito user pool as the authorization server. Cognito's metadata is where the flow ends:

```bash
curl -s https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX/.well-known/openid-configuration | jq '{registration_endpoint, code_challenge_methods_supported, scopes_supported}'
# {"registration_endpoint":null,"code_challenge_methods_supported":null,"scopes_supported":["openid","email","phone","profile"]}
```

With no `registration_endpoint`, Claude Code has no way to obtain a client ID and gives up before opening a browser:

```text
✘ Failed to connect — Incompatible auth server: does not support dynamic client registration
```

*Figure — DiscoveryFlow: Every hop succeeds until Claude Code reads Cognito's metadata, which has no registration endpoint to post to.*

Cognito also returns 400 for the RFC 8414 path `/.well-known/oauth-authorization-server`; it serves only the OpenID Connect `openid-configuration` document. That matters for the pre-registered route below.

The quickest way around all of this is a static header. It connects, but only until the token expires, and then it shows:

```text
Status: ✘ Failed to connect
Issue: Server rejected the configured Authorization header (HTTP 401). … Error detail: {"jsonrpc":"2.0","error":{"code":-32001,"message":"Token has expired"}}
```

## Browser sign-in with a pre-registered Cognito app client

**The MCP authorization spec puts pre-registration first**: a client that already holds a client ID for the authorization server should use it before trying anything else. Claude Code supports that through `oauth.clientId`, which skips the registration step entirely and leaves Cognito nothing it lacks.

The app client needs three things on the Cognito side:

1. The authorization code grant and no client secret. Claude Code is a public client and sends PKCE on its own.
2. A callback URL of `http://localhost:<port>/callback`. Cognito matches callback URLs exactly, so the port has to be fixed.
3. Its client ID listed in the AgentCore runtime's `allowedClients`, or the token it issues is rejected with a 401 that looks identical to a bad token.

Then register the server with the same port and an explicit metadata URL:

```bash
claude mcp add-json my-mcp-server '{
  "type": "http",
  "url": "https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/<URL-encoded runtime ARN>/invocations?qualifier=DEFAULT",
  "oauth": {
    "clientId": "YOUR_APP_CLIENT_ID",
    "callbackPort": 8765,
    "authServerMetadataUrl": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX/.well-known/openid-configuration",
    "scopes": "openid email profile"
  }
}'
```

`callbackPort` makes Claude Code listen on the port the app client already trusts instead of a random one. `authServerMetadataUrl` points it straight at the document Cognito actually serves.

The server now shows as needing authentication rather than as failed, and Authenticate opens the Cognito Hosted UI:

*Image: Claude Code's /mcp server list, with the Local MCPs group boxed in red: one server connected with 5 tools, and an -oauth server marked "needs authentication"*

*Image: Claude Code's /mcp detail view for the server, showing status "needs authentication", auth "not authenticated", and an Authenticate option*

*Image: The Amazon Cognito Hosted UI sign-in form with username and password fields*

After signing in, the browser hands the code back to Claude Code on the callback port:

*Image: Claude Code output after /mcp reading "Authentication successful. Connected to", with the server name blurred*

A tool call through the connected server returned results, which confirms that AgentCore accepted the token and not only that Claude Code stored one.

### `invalid_scope` until the scopes are pinned

**Without `oauth.scopes`, the browser never reaches a working sign-in.** It lands back on Claude Code's callback page with an error from Cognito:

*Image: Claude Code's browser callback page reading "Authentication failed" with the error "invalid_request: invalid_scope"*

With `authServerMetadataUrl` set, Claude Code takes the scopes to request from the `scopes_supported` field of `openid-configuration`. That file is the user pool's discovery document, in the format the OpenID Connect standard defines, and it lists every scope the pool supports: `openid email phone profile`.

Each app client allows its own subset of those, and this one allowed only `openid email profile`. The request asked for `phone`, and Cognito refused it.

Pinning `"scopes": "openid email profile"` to match the app client's allowed scopes fixed it. Any scope list the app client fully allows works; what fails is requesting one it does not.

## A static header that refreshes itself with `headersHelper`

`headersHelper` is a command whose standard output Claude Code uses as request headers. It runs on every connection, and when a tool call comes back 401 or 403, Claude Code runs it again, reconnects, and retries once. **A helper that mints a fresh Cognito token makes the expiry invisible.**

```bash title="scripts/mcp_headers.sh"
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
source .cognito.env   # COGNITO_CLIENT_ID, COGNITO_USERNAME, COGNITO_PASSWORD, AWS_REGION

TOKEN=$(aws cognito-idp initiate-auth --no-sign-request \
  --client-id "$COGNITO_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters "USERNAME=${COGNITO_USERNAME},PASSWORD=${COGNITO_PASSWORD}" \
  --region "$AWS_REGION" \
  --query 'AuthenticationResult.AccessToken' --output text)

printf '{"Authorization": "Bearer %s"}\n' "$TOKEN"
```

`InitiateAuth` needs no AWS request signing, so `--no-sign-request` lets the script run with no AWS credentials at all. Otherwise the script would have to fetch AWS credentials through a credential broker, which can stop at an MFA prompt and wait for a person. The helper has a 10-second timeout, so a prompt nobody answers in time fails the connection.

Two more requirements:

- The app client needs `USER_PASSWORD_AUTH` enabled.
- AgentCore requires the **access token**, since it checks the token's `client_id` claim against `allowedClients`.

```bash
claude mcp add-json my-mcp-server '{
  "type": "http",
  "url": "https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/<URL-encoded runtime ARN>/invocations?qualifier=DEFAULT",
  "headersHelper": "/absolute/path/to/scripts/mcp_headers.sh"
}'
```

The first registration printed a warning instead of connecting:

```text
MCP server 'my-mcp-server': headersHelper not run — this workspace has no persisted trust; accept the trust dialog here once interactively
```

**A helper is a command Claude Code executes, so it only runs in a workspace you have trusted.** Claude Code's documentation describes it as "an arbitrary shell command".

Starting `claude` interactively in that directory once and accepting the trust dialog was enough, and the server connected on the next start.

The trade-off against the pre-registered client is where the credential lives. The helper needs a password on disk and an app client with password auth enabled, while the OAuth route keeps only a refresh token that Claude Code stores. The helper works headless and in scripts, where nobody is present to complete a browser sign-in.

## If you need real dynamic client registration

Pre-registration means every user's Claude Code is configured with a client ID ahead of time. That is fine for one team and awkward for a server meant to be added by anyone with its URL.

For clients with no prior relationship, the current MCP authorization spec prefers **Client ID Metadata Documents** (CIMD) and demotes dynamic registration to a backwards-compatibility option. In CIMD the client ID is an HTTPS URL to a JSON document describing the client, and the authorization server fetches it.

Claude Code already uses it when a server advertises `client_id_metadata_document_supported`, but **Cognito does not advertise it, so CIMD is not available here**.

That leaves three workarounds. **None of them were tried here**; they are the ones other Cognito users and AWS's AgentCore guide point to. Only the `/register` endpoint keeps both Cognito and AgentCore's inbound JWT check.

1. **A `/register` endpoint in front of Cognito.** A small service accepts the RFC 7591 request, calls `CreateUserPoolClient`, and returns the new client ID, alongside a metadata document that adds the `registration_endpoint`. The cost is a public endpoint backed by an admin API: it needs rate limiting, a redirect URI allowlist, and cleanup, since a user pool has a quota on app clients. Each new client would also have to reach AgentCore's `allowedClients`.
2. **An OAuth proxy.** FastMCP's Cognito integration answers the dynamic registration itself and forwards everything to one real Cognito app client, so Cognito never sees per-client registrations. This fits a server built on FastMCP's own auth layer rather than AgentCore's inbound JWT check.
3. **An identity provider that supports it.** AWS's AgentCore Runtime MCP guide demonstrates automatic OAuth with Auth0 rather than Cognito, which is the shortest route when the choice of provider is still open.

## Summary

- Claude Code's default OAuth route needs dynamic client registration, and Cognito publishes no `registration_endpoint`, so it fails with `Incompatible auth server`.
- **A pre-registered Cognito app client gives browser sign-in**: set `oauth.clientId`, a `callbackPort` matching the app client's callback URL, `authServerMetadataUrl` pointing at `openid-configuration`, and `oauth.scopes` limited to what the app client allows.
- **`headersHelper` gives an auto-refreshing header**: a script that calls `initiate-auth --no-sign-request` and prints the access token, run in a trusted workspace.
- A static `Authorization` header disables the OAuth fallback and lasts one token lifetime.
- For registration with no prior setup, the MCP authorization spec now prefers CIMD, which Cognito does not support. With Cognito and AgentCore together, a `/register` shim is the only option that works directly; otherwise, switch to a different identity provider.

## References

- [MCP specification 2025-11-25: Authorization, including client registration priority and Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
- [Claude Code docs: Connect Claude Code to tools via MCP, including pre-configured OAuth credentials and headersHelper](https://code.claude.com/docs/en/mcp)
- [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
- [RFC 9728: OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
- [OpenID Connect Discovery 1.0: the format of the `openid-configuration` document](https://openid.net/specs/openid-connect-discovery-1_0.html)
- [OAuth Client ID Metadata Document draft](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00)
- [FastMCP: AWS Cognito OAuth integration using an OAuth proxy](https://gofastmcp.com/integrations/aws-cognito)
- [Amazon Bedrock AgentCore: Authenticate and authorize with Inbound Auth and Outbound Auth](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html)
