Sitemap

Cognito-based MCP Servers

9 min readAug 26, 2025

--

Press enter or click to view image in full size

How to implement an MCP server supporting OAuth using AWS Cognito. In this article, I would like to show you not only how to do it, but also provide a step-by-step guide on working with it. Authentication is changing a lot in the MCP area. If you don’t believe me, check the FastMCP warning on the Authentication page. Keep that in mind as it might change over time. I will try to keep this article updated.

Press enter or click to view image in full size
https://gofastmcp.com/servers/auth/authentication

Pirate ship, AKA what we want to achieve

Working VSCode Flow
Working MCP Inspector Flow

Root flow

Let’s assume our MCP server is exposed under http://localhost:8080/mcp. In this scenario, our resource server will also act as an authorization server. How does it work? Let’s start from the beginning. Let’s create our test environment.

Running debugger

We can do so by running MCP Inspector with a command:

npx @modelcontextprotocol/inspector

After that, we will see the welcome MCP Inspector screen where we can point it to http://localhost:8080/mcp like on the screen below. There is an extra info there about Auth section.

Press enter or click to view image in full size

After clicking this button, we can see a view where we can play with the whole auth flow, which is super helpful whenever you are implementing such a flow (which might be complicated).

Press enter or click to view image in full size

We have an option to use a Guided OAuth Flow flow that goes through the OAuth flow step-by-step, so it might be easier for you as an engineer to follow what is going on and setup endpoints to correctly set it. Another option is to go with Quick OAuth Flow that will automate going through all steps on your behalf, which is very useful when you are trying to set up the correct auth and you are retrying for the 123562153761725 time.

Since we are starting with an empty MCP Server, we have no OAuth endpoints there yet. So after starting the flow, we can get (as expected) an error.

Press enter or click to view image in full size

Specification

The whole flow is described in the official MCP Documentation. Since we have /mcp set in the inspector as our server MCP resource, it is being used according to the 1st flow described below in the specs.

Press enter or click to view image in full size
https://modelcontextprotocol.io/specification/draft/basic/authorization#server-metadata-discovery

Metadata Discovery

The MCP Inspector attempts to determine how your server is protected and attempts to load the advertised authentication flow. It is proceeding with multiple requests being sent to your server that try to discover right endpoint. Here is an example flow executed by the MCP inspector.

Press enter or click to view image in full size
Protected resource discovery

Protected resource redirection

Before we go to the implementation. Let’s think for a second about how our server consumer is actually redirected to discover our server metadata in real life, not triggered over the inspector, which is a fake OAuth start attempt.

When we are trying to connect to our Server, the client is trying to initialize a connection with the MCP server by calling it over the /mcp endpoint. When our server validates that requests and the token is missing or invalid, we have to return an error with a 401 status and a WWW-Authenticate header with information about the protected resource endpoint.

WWW-Authenticate: Bearer resource_metadata=\"http://localhost:8080/.well-known/oauth-protected-resource/mcp\"

After implementing the OAuth middleware, which validates the correct Authorization token, our flow will evolve a bit. It has just started with a connection attempt, followed by the exact same discovery. So we can assume that we can progress from there using the MCP inspector OAuth flow debugger.

Press enter or click to view image in full size

Metadata Resource Discovery implementation

We now need to expose the endpoint that will serve protected resource information, as specified in the RFC here.

We can implement a response that returns the one below. Authorization server will be exposed under / and our resource (MCP endpoints) under /mcp with additional info about supported scopes.

GET http://localhost:8080/.well-known/oauth-protected-resource/mcp
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 25 Aug 2025 13:03:12 GMT
Content-Length: 177
{
"authorization_servers": [
"http://localhost:8080"
],
"bearer_methods_supported": [
"header"
],
"resource": "http://localhost:8080/mcp",
"scopes_supported": [
"openid",
"profile",
"email"
]
}

So now, we are returning 200 here, and our flow evolves into flow with limited tries of protected resource discovery, but it still fails the whole discovery process.

Press enter or click to view image in full size

Metadata OpenID implementation

Since we are using Cognito, we can implement returning the OpenID configuration. We can have an implementation that loads data from the official Cognito well-known URL https://cognito-idp.<region-id>.amazonaws.com/<user-pool-id>/.well-known/openid-configuration, extends it, and returns it to our MCP Client.

We want to add a couple of customizations that will be important in the next steps. Most important things:

  • Override token_endpoint as we don’t want our clients to provide client secret explicitly in case of not going with DCR flow
  • Advertize registration_endpoint to not fail clients that depend on DCR flow
Press enter or click to view image in full size

Example response could look like that:

GET http://localhost:8080/.well-known/openid-configuration
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 25 Aug 2025 13:52:53 GMT
Content-Length: 1540
{
"authorization_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/oauth2/authorize",
"client_id": "69eahpaqpmtbgljh6bufp42f0",
"code_challenge_methods_supported": [
"S256"
],
"end_session_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/logout",
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"issuer": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_b2xK16XUA",
"jwks_uri": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_b2xK16XUA/.well-known/jwks.json",
"redirect_uris": [
"http://127.0.0.1:33418",
"http://127.0.0.1:35535/oauth/callback",
"http://localhost:3080/api/mcp/mcp-local-api/oauth/callback",
"http://localhost:3080/c/new",
"http://localhost:3080/oauth/openid/callback",
"http://localhost:33418",
"http://localhost:35535/oauth/callback",
"http://localhost:55570/oauth/callback",
"http://localhost:6274/oauth/callback",
"http://localhost:6274/oauth/callback/debug",
"http://localhost:8080",
"https://vscode.dev/redirect"
],
"registration_endpoint": "http://localhost:8080/register",
"response_types_supported": [
"code",
"token"
],
"revocation_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/oauth2/revoke",
"scopes_supported": [
"openid",
"email",
"phone",
"profile"
],
"subject_types_supported": [
"public"
],
"token_endpoint": "http://localhost:8080/oauth/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"userinfo_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/oauth2/userInfo"
}

Now, a small success! We got 1st green step!

Press enter or click to view image in full size

Dynamic Client Registration (DCR)

The next step on the MCP Inspector calls list is “Client registration”. The idea is that the MCP Server will automatically create a client for itself to reduce the friction in the authentication client registration process.

DCR is extremely useful, especially since different clients use various paths and ports, which can make management cumbersome. However, the question is whether you would like to add such functionality. Cognito does not support it out of the box, so you will have to implement it yourself. How could this be implemented if we were to support it? We can invoke the CreateUserPoolClient API based on the request input and return the clientId to the customer along with the clientSecret.

The client will invoke your server with a similar payload to the one below, so you can easily create a Cognito client if you wish.

POST /register HTTP/1.1
{
"redirect_uris": [
"http://localhost:6274/oauth/callback/debug"
],
"token_endpoint_auth_method": "none",
"grant_types": [
"authorization_code",
"refresh_token"
],
"response_types": [
"code"
],
"client_name": "MCP Inspector",
"client_uri": "https://github.com/modelcontextprotocol/inspector",
"scope": "openid profile email"
}

I am not a big fan of this approach. I agree with Aaron Parecki that “The problem is most commercial APIs are not going to enable Dynamic Client Registration on their production servers. For example, in order to get client credentials to use the Google APIs, you need to register as a developer and then register an OAuth client after logging in. Dynamic Client Registration would allow a client to register itself without the link to the developer’s account. That would mean there is no paper trail for who the client was developed by. The Dynamic Client Registration endpoint can’t require authentication by definition, so is a public endpoint that can create clients, which as you can imagine opens up some potential security issues.” If you’ve got interested, the whole article can be found here.

Bypass DCR

So, how can we bypass DCR? MCP Docs suggests that we can hardcode that information on the client end. We can do something better.

Press enter or click to view image in full size
source: https://modelcontextprotocol.io/specification/draft/basic/authorization#dynamic-client-registration

We can dynamically load details of our Cognito client and serve them to the client while removing the secret on the fly. That way, we won’t be forced to hardcode that client ID here and there while still “having support” (fake one) for the clients requiring DCR.

HTTP/1.1 201 Created
Connection: close
Access-Control-Allow-Origin: *
Content-Type: application/json
{
"client_id": "69eahpaqpmtbgljh6bufp42f0",
"client_id_issued_at": 1756130873,
"client_name": "MCP Inspector",
"client_secret_expires_at": 0,
"client_uri": "https://github.com/modelcontextprotocol/inspector",
"grant_types": [
"authorization_code",
"refresh_token"
],
"redirect_uris": [
"http://localhost:6274/oauth/callback/debug"
],
"response_types": [
"code"
],
"scope": "openid profile email",
"token_endpoint_auth_method": "none"
}

Now, after returning that response under /register our flow works up to this point!

Press enter or click to view image in full size

Another step green! Great!

Press enter or click to view image in full size

Token authorization

In the previous step, we have overridden the authorization endpoint under token_endpoint and now we are going to implement it.

We are going to “intercept” the request to the Cognito token endpoint and extend its payload with client_secret and send it to the actual Cognito to exchange it for tokens.

Request we receive:

POST /oauth/token HTTP/1.1
grant_type=authorization_code&code=<some_code>&code_verifier=<some_code_verifier>&redirect_uri=http%3A%2F%2Flocalhost%3A6274%2Foauth%2Fcallback%2Fdebug&client_id=69eahpaqpmtbgljh6bufp42f0&resource=http%3A%2F%2Flocalhost%3A8080%2Fmcp

After calling the Cognito token with the same form values, extended with client_secret We are returning a response like the one below:

HTTP/1.1 200 OK
Connection: close
Access-Control-Allow-Origin: *
Content-Type: application/json
{
"access_token": "...",
"expires_in": 3600,
"id_token": "...",
"refresh_token": "...",
"token_type": "Bearer"
}

Hooray! Full flow is green!

Press enter or click to view image in full size

Finally, our flow looks like this:

Press enter or click to view image in full size

First full MCP Inspector connection

So now we can click connect and see access to the tools we have exposed in our MCP server.

Press enter or click to view image in full size

Fantastic! Now let’s test the connection with VSCode. Let’s use the configuration below:

{
"servers": {
"auth-mcp": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
},
"inputs": []
}

Our invocation flow will look like the diagram below:

Press enter or click to view image in full size

Thanks to that, finally, we can see the protected tool working fine. Thanks to the Cognito context and groups, we can filter permissions and visibility of the tools.

Press enter or click to view image in full size

Of course, this flow will also work for other types of clients, such as LibreChat with Ollama.

Press enter or click to view image in full size

Summary… and words of warning

There are many quirks in the clients when it comes to using the context path. If you would like to expose your MCP server under a load balancer or common gateway, you may encounter unusual behaviors and inconsistencies. Because of that, I think I will go with a dedicated auth service for MCPs in the way that PayPal did. Sometimes those flows work a bit differently across VS Code, MCP Inspector, and LibreChat, and I believe that the words MUST and SHOULD in the specs make no difference in many situations. Regarding those different path scans, you can take a look at them as well in the specs:

Press enter or click to view image in full size
Press enter or click to view image in full size
https://datatracker.ietf.org/doc/html/rfc8414#section-5

It might be tricky sometimes… but hey! Now you know how it works and how to deal with it :) Good luck!

Materials:

There was not much space for implementation details that you can find here. Code is written in Go as a draft: https://github.com/SodaDev/mcp-auth

--

--