Third-party OAuth Provider
WIP
Refer to the GitHub OAuth Provider example ↗ for a complete example of how to use a third-party OAuth provider with the MCP Server SDK.
import OAuthProvider, { AuthRequest, OAuthHelpers,} from "workers-oauth-provider";import { MCPEntrypoint } from "./lib/MCPEntrypoint";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";import { Hono } from "hono";import pick from "just-pick";import { Octokit } from "octokit";
// Context from the auth process, encrypted & stored in the auth token// and provided to the MCP Server as this.propstype Props = { login: string; name: string; email: string; accessToken: string;};
export class MyMCP extends MCPEntrypoint<Props> { get server() { const server = new McpServer({ name: "Github OAuth Proxy Demo", version: "1.0.0", });
server.tool( "add", "Add two numbers the way only MCP can", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }), );
server.tool( "whoami", "Tasty props from my OAuth provider", {}, async () => ({ content: [ { type: "text", text: JSON.stringify(pick(this.props, "login", "name", "email")), }, ], }), );
server.tool( "userInfoHTTP", "Get user info from GitHub, via HTTP", {}, async () => { const res = await fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${this.props.accessToken}`, "User-Agent": "04-auth-pivot", }, }); return { content: [{ type: "text", text: await res.text() }] }; }, );
server.tool( "userInfoOctokit", "Get user info from GitHub, via Octokit", {}, async () => { const octokit = new Octokit({ auth: this.props.accessToken }); return { content: [ { type: "text", text: JSON.stringify(await octokit.rest.users.getAuthenticated()), }, ], }; }, ); return server; }}
const app = new Hono<{ Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } }>();
/** * OAuth Authorization Endpoint * * This route initiates the GitHub OAuth flow when a user wants to log in. * It creates a random state parameter to prevent CSRF attacks and stores the * original OAuth request information in KV storage for later retrieval. * Then it redirects the user to GitHub's authorization page with the appropriate * parameters so the user can authenticate and grant permissions. */app.get("/authorize", async (c) => { const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw); // Store the request info in KV to catch ya up on the rebound const randomString = crypto.randomUUID(); await c.env.OAUTH_KV.put( `login:${randomString}`, JSON.stringify(oauthReqInfo), { expirationTtl: 600 }, );
const upstream = new URL(`https://github.com/login/oauth/authorize`); upstream.searchParams.set("client_id", c.env.GITHUB_CLIENT_ID); upstream.searchParams.set( "redirect_uri", new URL("/callback", c.req.url).href, ); upstream.searchParams.set("scope", "read:user"); upstream.searchParams.set("state", randomString); upstream.searchParams.set("response_type", "code");
return Response.redirect(upstream.href);});
/** * OAuth Callback Endpoint * * This route handles the callback from GitHub after user authentication. * It exchanges the temporary code for an access token, then stores some * user metadata & the auth token as part of the 'props' on the token passed * down to the client. It ends by redirecting the client back to _its_ callback URL */app.get("/callback", async (c) => { const code = c.req.query("code") as string;
// Get the oathReqInfo out of KV const randomString = c.req.query("state"); if (!randomString) { return c.text("Missing state", 400); } const oauthReqInfo = await c.env.OAUTH_KV.get<AuthRequest>( `login:${randomString}`, { type: "json" }, ); if (!oauthReqInfo) { return c.text("Invalid state", 400); }
// Exchange the code for an access token const resp = await fetch(`https://github.com/login/oauth/access_token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: c.env.GITHUB_CLIENT_ID, client_secret: c.env.GITHUB_CLIENT_SECRET, code, redirect_uri: new URL("/callback", c.req.url).href, }).toString(), }); if (!resp.ok) { console.log(await resp.text()); return c.text("Failed to fetch access token", 500); } const body = await resp.formData(); const accessToken = body.get("access_token"); if (!accessToken) { return c.text("Missing access token", 400); }
// Fetch the user info from GitHub const apiRes = await fetch(`https://api.github.com/user`, { headers: { Authorization: `bearer ${accessToken}`, "User-Agent": "04-auth-pivot", }, }); if (!apiRes.ok) { console.log(await apiRes.text()); return c.text("Failed to fetch user", 500); }
const user = (await apiRes.json()) as Record<string, string>; const { login, name, email } = user;
// Return back to the MCP client a new token const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ request: oauthReqInfo, userId: login, metadata: { label: name, }, scope: oauthReqInfo.scope, // This will be available on this.props inside MyMCP props: { login, name, email, accessToken, } as Props, });
return Response.redirect(redirectTo);});
export default new OAuthProvider({ apiRoute: "/sse", apiHandler: MyMCP.Router, defaultHandler: app, authorizeEndpoint: "/authorize", tokenEndpoint: "/token", clientRegistrationEndpoint: "/register",});
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark