// auth server to handle Gmail authentication for the mcp server
import credentials from "../credentials.json";
import { AUTH_PORT, AUTH_SERVER_URL } from "./config";
const { web } = credentials;
const { client_id, client_secret, auth_uri, token_uri, redirect_uris } = web;
const redirectUri = redirect_uris[0];
const oauthMetadata = {
issuer: AUTH_SERVER_URL,
authorization_endpoint: `${AUTH_SERVER_URL}/authorize`,
token_endpoint: `${AUTH_SERVER_URL}/token`,
registration_endpoint: `${AUTH_SERVER_URL}/register`,
introspection_endpoint: `${AUTH_SERVER_URL}/introspect`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
code_challenge_methods_supported: ["S256"],
scopes_supported: ["gmail.readonly", "gmail.compose"],
};
async function main() {
const states = new Map<
string,
{
clientId?: string;
redirectUri?: string;
mcpState?: string;
}
>();
const codes = new Map<
string,
{
tokenData: {
access_token: string;
refresh_token?: string;
expires_in: number;
scope: string;
};
scope: string;
clientId?: string;
}
>();
// Store tokens (for validation later by /introspection endpoint)
const tokens = new Map<
string,
{
tokenData: { access_token: string; refresh_token?: string };
scope: string;
expiresAt: number;
}
>();
const server = Bun.serve({
port: AUTH_PORT,
routes: {
// oauth metadata discovery
// RFC 8414: https://datatracker.ietf.org/doc/html/rfc8414
"/.well-known/oauth-authorization-server": {
GET: (req) => Response.json(oauthMetadata),
},
"/authorize": {
GET: (req) => {
const url = new URL(req.url);
const mcpClientId = url.searchParams.get("client_id");
const mcpRedirectUri = url.searchParams.get("redirect_uri");
const mcpState = url.searchParams.get("state");
const state = crypto.randomUUID();
const params = {
client_id,
scope: "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.compose",
response_type: "code",
state,
redirect_uri: redirectUri,
access_type: "offline",
};
states.set(state, {
clientId: mcpClientId || undefined,
redirectUri: mcpRedirectUri || undefined,
mcpState: mcpState || undefined,
});
const authUrl = new URL(auth_uri);
authUrl.search = new URLSearchParams(params).toString();
return Response.redirect(authUrl.toString(), 302);
},
},
"/callback": {
GET: async (req) => {
const url = new URL(req.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const stateData = states.get(state);
if (!stateData) {
return Response.json(
{ error: "Invalid state" },
{ status: 400 },
);
}
// use the code to exchange for a token
const tokenUrl = new URL(token_uri);
const params = {
client_id,
code,
client_secret,
grant_type: "authorization_code",
redirect_uri: redirectUri,
};
// exchange token url
const tokenResponse = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams(params),
});
if (!tokenResponse.ok) {
return Response.json(
{ error: "Token exchange failed" },
{ status: 500 },
);
}
const tokenData: {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
} = await tokenResponse.json();
const authCode = crypto.randomUUID();
codes.set(authCode, {
tokenData,
scope: tokenData.scope,
clientId: stateData.clientId,
});
// delete the state now that it has been used
states.delete(state);
// Redirect back to Claude's callback URL with the auth code
if (stateData.redirectUri) {
const redirectUrl = new URL(stateData.redirectUri);
redirectUrl.searchParams.set("code", authCode);
if (stateData.mcpState) {
redirectUrl.searchParams.set(
"state",
stateData.mcpState,
);
}
return Response.redirect(redirectUrl.toString(), 302);
}
return Response.json({ code: authCode });
},
},
"/token": {
POST: async (req) => {
const body = await req.formData();
const grantType = body.get("grant_type");
const authCode = body.get("code");
if (grantType === "authorization_code") {
const codeData = codes.get(authCode);
if (!codeData) {
return Response.json(
{ error: "invalid_grant" },
{ status: 400 },
);
}
const accessToken = crypto.randomUUID();
// store for validation later
tokens.set(accessToken, {
tokenData: codeData.tokenData,
scope: codeData.scope,
expiresAt:
Date.now() +
codeData.tokenData.expires_in * 1000,
});
// once we've validated the code, delete it
codes.delete(authCode);
return Response.json({
access_token: accessToken,
token_type: "Bearer",
expires_in: codeData.tokenData.expires_in,
scope: codeData.scope,
});
}
return Response.json(
{ error: "unsupported_grant_type" },
{ status: 400 },
);
},
},
// token introspection endpoint used by the client to validate tokens
"/introspect": {
POST: async (req) => {
const body = await req.formData();
const token = body.get("token");
const tokenData = tokens.get(token);
if (!tokenData || Date.now() > tokenData.expiresAt) {
return Response.json({ active: false });
}
return Response.json({
active: true,
scope: tokenData.scope,
exp: Math.floor(tokenData.expiresAt / 1000),
// Return the actual Google access token so we can make API calls
google_access_token: tokenData.tokenData.access_token,
});
},
},
// Dynamic Client Registration (RFC 7591)
"/register": {
POST: async (req) => {
const body = await req.json();
const clientId = crypto.randomUUID();
return Response.json({
client_id: clientId,
client_name: body.client_name || "MCP Client",
redirect_uris: body.redirect_uris || [],
grant_types: body.grant_types || ["authorization_code"],
response_types: body.response_types || ["code"],
token_endpoint_auth_method:
body.token_endpoint_auth_method || "none",
});
},
},
},
});
console.error(`Server running at ${server.url}`);
}
main().catch((error) => {
console.error("Fatal error in main():", error);
if (error instanceof Error && error?.code.includes("EADDRINUSE")) {
console.error(
"Port already in use. Try using lsof -i :${PORT} to see what is running on that port.",
);
}
process.exit(1);
});