How to Understand OAuth Flows Between MCP Server and Client with Cloudflare Workers and MCP Inspector -- Step by Step
When building an MCP Server, providing user-specific tools and services requires user registration and login. Once a user is authenticated, their user ID can be used to query account data or perform more advanced operations. Therefore, user authentication is a crucial step for enabling personalized and in-depth features on the MCP Server.
For communication between service interfaces, a common and straightforward authentication method is to include an access token in the Authorization header of the request, using the format:
Authorization: Bearer XXXXX.
The validity of the AccessToken determines the user’s identity. Typically, the access token is issued by the MCP Server provider after the user has successfully registered with the MCP service. Once obtained, this token can be included in API requests to access authorized features and data on the MCP Server.
Deploying the MCP Server as a Cloud Service
Cloudflare, as a cloud service provider, offers the ability to build an MCP Server using its edge computing platform — Cloudflare Workers. This enables developers to deploy an MCP Server quickly and easily, without managing traditional backend infrastructure.
Cloudflare provides official Worker examples demonstrating how to build remote MCP services with third-party OAuth providers such as Google and GitHub, making it straightforward to integrate authentication and deploy scalable, serverless MCP solutions.
You can deploy your own MCP Server simply by following the official .
One important note: Server-Sent Events (SSE) has been deprecated and will be replaced by StreamableHttp as the primary method for remote connections going forward.
To support StreamableHttp, you’ll need to add an additional apiHandlers entry to your Cloudflare Worker to handle the new streaming connection logic.
//index.ts
export default new OAuthProvider({
apiHandlers: {'/mcp': MyMCP.serve('/mcp') as any,
'/sse': MyMCP.serveSSE('/sse') as any},
})
After deployment, you can test the connection using the auto-generated service URL in Cloudflare’s official Playground:
👉 https://playground.ai.cloudflare.com/
Alternatively, you can debug and interact with your MCP service locally using the MCP Inspector:
npx @modelcontextprotocol/inspector
⚠️ Debugging Local Inspector Connections
While testing with the Inspector, I initially encountered connection issues — the default Worker endpoint (e.g., xxxxx-worker.dev) was not reachable from the local Inspector, even though it worked fine in the Playground.
To resolve this, I bound the Worker to a custom domain (e.g., mcp.example.com), and the connection via Inspector started working as expected. It appears that remote tools like the Inspector may not support default *.dev subdomains for secure external connections.
Seamless Connection Flow
Whether you’re using the Cloudflare Playground or the MCP Inspector, the connection flow feels smooth and intuitive.
Here’s what the typical user experience looks like:
The user initiates a connection to the MCP that requires authorization by clicking a link. The Client displays a prompt window.
The user clicks “Authorize” and is redirected to a third-party OAuth page to log in and grant access.
Upon successful authorization, the third-party provider redirects back to the Client, which now receives access to the MCP — including a list of available tools associated with the user.
A Closer Look at the OAuth Flow
As developers, once we’ve successfully used the tools and achieved the expected results, it’s essential to take a step back and understand the underlying principles and implementation. OAuth is a well-established protocol that has been widely adopted across countless applications and services.
So how is it applied in the context of MCP?
Fortunately, Cloudflare provides a well-documented explanation of how OAuth is integrated into MCP’s remote server flow, detailing how authorization, token exchange, and secure access are handled behind the scenes.
At the heart of this process is the sequence diagram that illustrates the core flow.
OAuth Flow Steps
MCP Client → MCP Server: Sends an MCP request.
MCP Server → MCP Client: Responds with 401 Unauthorized.
MCP Client: Generates code_verifier and code_challenge.
MCP Client → Browser: Opens the authorization URL (with code_challenge).
Browser → MCP Server: Navigates to /authorize.
User: Logs in and grants authorization.
MCP Server → Browser: Redirects to the callback URL with an authorization code.
Browser → MCP Client: Passes the authorization code to the client.
MCP Client → MCP Server: Sends a token request (with code and code_verifier).
MCP Server → MCP Client: Returns Access Token and Refresh Token.
MCP Client → MCP Server: Sends a new MCP request with the Access Token.
MCP message exchange begins successfully.
Let’s take a closer look at this process by exploring the Inspector tool and the example code provided by Cloudflare.
The core of the implementation relies on the official MCP TypeScript SDK, @modelcontextprotocol/sdk. Authorization is handled using the 'use client' approach in the SDK.
The MCP service endpoint we’ll connect to is:
https://mcp.example.com/mcp via StreamableHttp.
1. Sends an MCP request.
The MCP Client needs the Server’s URL to initiate requests. Since Server-Sent Events (SSE) will no longer be supported, StreamableHttp is adopted as the default connection method going forward. This article focuses on the StreamableHttp approach. However, the SSE method is still supported and fully encapsulated in Cloudflare’s examples and the Inspector tool—you can refer to their source code for details.
Below is an example showing how to declare the Client, TransportOptions, and Transport, followed by establishing a connection using the Client.
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
const headers: HeadersInit = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: "1.0.0",
},
{
capabilities: {
sampling: {},
roots: {
listChanged: true,
},
},
},
);
let transportOptions = {
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init: RequestInit | undefined,
) => fetch(url, { init, headers }),
},
requestInit: {
headers,
},
// TODO these should be configurable...
reconnectionOptions: {
maxReconnectionDelay: 30000,
initialReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2,
},
};
const transport = new StreamableHTTPClientTransport(url, {
sessionId: undefined,
transportOptions,
});
try {
await client.connect(transport as Transport);
return client;
} catch (error) {
console.log(error)
}
2. Responds with 401 Unauthorized.
If the MCP server requires authorization, attempting to connect without proper authorization will result in a 401 Unauthorized error.
3. Generates code_verifier and code_challenge.
Handling 401 Errors
When connecting to an MCP server that requires authorization, you may receive a 401 Unauthorized error if the client is not yet authorized. Here’s a utility function to detect such errors:
const is401Error = (error: unknown): boolean => {
return (
(error instanceof SseError && error.code === 401) ||
(error instanceof Error && error.message.includes("401")) ||
(error instanceof Error && error.message.includes("Unauthorized"))
);
};
The authorization process is already encapsulated in the MCP TypeScript SDK, so you just need to call the appropriate method. The authProvider extends the OAuthClientProvider, adding support for storing variables or metadata. You can check the implementation here:
Example usage:
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
const authProvider = new InspectorOAuthClientProvider(sseUrl);
const result = await auth(authProvider, { serverUrl: sseUrl });
This function ultimately creates an authorized connection for you, handling important details like the code_challenge and redirect_uri.
The authorization page URL, e.g.,
https://mcptest.example.com/authorize, is implemented by the MCP Server.
code_challenge is used by the server to issue the authorization code.
code_challenge_method specifies the encryption algorithm used (e.g., S256).
redirect_uri is the callback URL where the authorization response will be sent.
Example authorization URL:
https://mcptest.example.com/authorize?
response_type=code
&client_id=iEwu9XI7upA5Gana
&code_challenge=iCzvWcmoHxPqrh69zT-bwSLYfs1IWP-pOpL4kyxMgyo
&code_challenge_method=S256
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fmcp%2Foauth%2Fcallback
4. Opens the authorization URL (with code_challenge).
The browser navigates to the authorization page via the connection URL.
The Inspector tool uses a same-window redirect for authorization, while the Playground opens the authorization page in a new window. Both methods work fine as long as the callback URL can properly handle returning the authentication data back to the main process.
Important: Google’s OAuth authorization requires a separate window and cannot be embedded inside an iframe; otherwise, it will return a 403 Forbidden error.
Within the authProvider, the redirection is implemented by overriding the following method to perform the window navigation.
redirectToAuthorization(authorizationUrl: URL) {
window.location.href = authorizationUrl.href;
}
5. Navigates to /authorize.
MCP Server Implements the Authorization Page and Flow (Using Google as an Example)
When a GET request is made to the /authorize endpoint, the MCP Server presents the authorization page. The server checks whether the user has already granted authorization. If authorization has been previously granted, the server returns the user’s credentials and redirects to the specified callback URL.
6. Logs in and grants authorization.
User Grants Authorization via the Google Authorization Page
If the user has not yet authorized access, the MCP Server displays an authorization prompt. When the user clicks “Approve,” they are redirected to Google’s OAuth flow to complete the authentication and authorization process.
7. Redirects to the callback URL with an authorization code.
Third-Party (Google) Callback to MCP Server
After the user completes the authorization with Google, Google redirects to the MCP Server’s /callback endpoint, providing the authorization code and user information. The MCP Server then redirects to the callback URL specified by the Client, completing the OAuth handshake.
Reference implementation:
8. Passes the authorization code to the client.
The /callback page configured by the client receives the authorization code and then forwards it back to the MCP Client to complete the authentication process.
const params = parseOAuthCallbackParams(window.location.search);
9. Sends a token request (with code and code_verifier)
MCP Client Requests /token from MCP Server to Exchange Code for Access Token
After receiving the authorization code on the callback page, the MCP Client can use it to call the MCP Server’s /token endpoint and retrieve an access token.
result = await auth(serverAuthProvider, {
serverUrl,
authorizationCode: params.code,
});
const tokens = await serverAuthProvider.tokens();
10. Returns Access Token and Refresh Token.
Once the access token is received, it indicates that the authorization was successful. The client can now use the access token to authenticate and perform subsequent operations with the MCP Server.
if (!tokens?.access_token) {
return notifyError('No access token received');
}
toast("Success", {
description: "Successfully authenticated with OAuth",
});
11. Sends a new MCP request with the Access Token.
After receiving the Access Token, include it in the Authorization header within the TransportOption. Then, re-initiate the connection to the MCP Server.
⚠️ Note: This is a time-consuming operation, so ensure it is handled asynchronously and with proper user feedback if used in UI flows.
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
await client.connect(transport as Transport);
12. MCP message exchange begins successfully
Once the MCP Client successfully connects using the valid AccessToken, it can now retrieve the available tools and capabilities exposed by the MCP Server.
const tools = await client.listTools();
console.log('Available tools:', tools);
This marks the completion of the authentication and connection lifecycle:
🔐 User Authorization → OAuth flow completed via third-party (e.g., Google)
🔑 Access Token Retrieval → Received from MCP Server
🔗 Secure Connection → Reconnected with token
🧰 Tool Discovery → MCP Server returns list of available tools for the authorized user
At this point, your client is authenticated, connected, and ready to interact with the server’s AI tools and services in a secure, personalized session.
SUMMARY
OAuth is a widely adopted authorization protocol used across various application scenarios. The MCP Server and Client TypeScript SDKs both provide working demo implementations of this flow. Overall, the user experience is smooth and well-integrated. However, there is currently no dedicated documentation that explains the entire process in detail.
In this post, we walked through and analyzed the full OAuth flow by combining two key resources:
A custom MCP Server implementation using Cloudflare Workers, and
The auth logic from Inspector, the official MCP debugging tool.
This end-to-end breakdown aims to help developers better understand and implement this part of the system when integrating MCP into their own applications.