Skip to main content
Glama
main.ts8.6 kB
#!/usr/bin/env node import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express, {type Request, type Response} from 'express'; import {createServer} from './index.js'; import type { OAuthMetadata, OAuthProtectedResourceMetadata, OAuthClientInformationFull, OAuthClientMetadata, } from '@modelcontextprotocol/sdk/shared/auth.js'; // Google OAuth configuration - users must provide their own credentials const {GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET} = process.env; const GOOGLE_AUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'; const GOOGLE_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'; const GMAIL_SCOPES = [ 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.modify', ]; function setupSignalHandlers(cleanup: () => Promise<void>): void { process.on('SIGINT', async () => { await cleanup(); process.exit(0); }); process.on('SIGTERM', async () => { await cleanup(); process.exit(0); }); } function extractBearerToken(req: Request): string | undefined { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return undefined; } return authHeader.slice(7); } const transport = process.env.MCP_TRANSPORT || 'stdio'; (async () => { if (transport === 'stdio') { const accessToken = process.env.GMAIL_ACCESS_TOKEN; if (!accessToken) { console.error('gmail-mcp: GMAIL_ACCESS_TOKEN required for stdio transport'); console.error('For OAuth support, use HTTP transport: MCP_TRANSPORT=http'); process.exit(1); } const server = createServer({token: accessToken}); setupSignalHandlers(async () => server.close()); const stdioTransport = new StdioServerTransport(); await server.connect(stdioTransport); console.error('Gmail MCP server running on stdio'); } else if (transport === 'http') { if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) { console.error('gmail-mcp: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET required for HTTP transport'); process.exit(1); } const app = express(); app.use(express.json()); app.use(express.urlencoded({extended: true})); const port = parseInt(process.env.PORT || '3000', 10); const baseUrl = process.env.MCP_BASE_URL || `http://localhost:${port}`; // OAuth Authorization Server Metadata (RFC 8414) // We act as the authorization server, proxying to Google const oauthMetadata: OAuthMetadata = { issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], code_challenge_methods_supported: ['S256'], scopes_supported: GMAIL_SCOPES, }; // Protected Resource Metadata (RFC 9728) const protectedResourceMetadata: OAuthProtectedResourceMetadata = { resource: `${baseUrl}/mcp`, authorization_servers: [baseUrl], scopes_supported: GMAIL_SCOPES, resource_name: 'Gmail MCP Server', resource_documentation: 'https://github.com/domdomegg/gmail-mcp', }; // Metadata endpoints app.get('/.well-known/oauth-authorization-server', (_req, res) => { res.json(oauthMetadata); }); app.get('/.well-known/oauth-protected-resource', (_req, res) => { res.json(protectedResourceMetadata); }); app.get('/.well-known/oauth-protected-resource/mcp', (_req, res) => { res.json(protectedResourceMetadata); }); // Dynamic Client Registration endpoint // We proxy through our /callback so any redirect URI works // Client ID/secret don't matter - we inject the real ones when proxying app.post('/register', (req: Request<object, object, OAuthClientMetadata>, res) => { const response: OAuthClientInformationFull = { ...req.body, client_id: 'gmail-mcp', client_id_issued_at: Math.floor(Date.now() / 1000), }; res.status(201).json(response); }); // Authorization endpoint - redirect to Google // We encode the client's redirect_uri in state so we can forward the code back app.get('/authorize', (req: Request, res: Response) => { const clientRedirectUri = typeof req.query.redirect_uri === 'string' ? req.query.redirect_uri : ''; const clientState = typeof req.query.state === 'string' ? req.query.state : ''; const codeChallenge = typeof req.query.code_challenge === 'string' ? req.query.code_challenge : ''; const codeChallengeMethod = typeof req.query.code_challenge_method === 'string' ? req.query.code_challenge_method : 'S256'; // Encode client's redirect_uri and state in our state parameter const wrappedState = Buffer.from(JSON.stringify({ redirect_uri: clientRedirectUri, state: clientState, })).toString('base64url'); const params = new URLSearchParams({ client_id: GOOGLE_CLIENT_ID, redirect_uri: `${baseUrl}/callback`, response_type: 'code', scope: GMAIL_SCOPES.join(' '), access_type: 'offline', prompt: 'consent', state: wrappedState, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod, }); res.redirect(`${GOOGLE_AUTH_ENDPOINT}?${params.toString()}`); }); // Callback endpoint - receives code from Google and forwards to client app.get('/callback', (req: Request, res: Response) => { const code = typeof req.query.code === 'string' ? req.query.code : ''; const wrappedState = typeof req.query.state === 'string' ? req.query.state : ''; const error = typeof req.query.error === 'string' ? req.query.error : ''; try { const {redirect_uri: clientRedirectUri, state: clientState} = JSON.parse(Buffer.from(wrappedState, 'base64url').toString()) as {redirect_uri: string; state: string}; const params = new URLSearchParams(); if (code) { params.set('code', code); } if (clientState) { params.set('state', clientState); } if (error) { params.set('error', error); } res.redirect(`${clientRedirectUri}?${params.toString()}`); } catch { res.status(400).json({error: 'invalid_state', error_description: 'Could not decode state parameter'}); } }); // Token endpoint - proxy to Google, injecting our client credentials app.post('/token', async (req: Request, res: Response) => { try { const body = new URLSearchParams({ ...req.body, client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET, redirect_uri: `${baseUrl}/callback`, }); const response = await fetch(GOOGLE_TOKEN_ENDPOINT, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: body.toString(), }); const data = await response.json(); res.status(response.status).json(data); } catch (error) { console.error('Token exchange error:', error); res.status(500).json({error: 'server_error', error_description: 'Token exchange failed'}); } }); // Stateless MCP endpoint app.post('/mcp', async (req: Request, res: Response) => { const token = extractBearerToken(req); // Require auth, except for tools/list for discovery const method = req.body?.method as string | undefined; if (!token && method !== 'tools/list') { res.status(401).json({ jsonrpc: '2.0', error: {code: -32001, message: 'Unauthorized: Bearer token required'}, id: null, }); return; } const server = createServer({token: token ?? ''}); try { const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(httpTransport); await httpTransport.handleRequest(req, res, req.body); res.on('close', () => { void httpTransport.close(); void server.close(); }); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: {code: -32603, message: 'Internal server error'}, id: null, }); } } }); const httpServer = app.listen(port, () => { console.error(`Gmail MCP server running on ${baseUrl}/mcp`); }); httpServer.on('error', (err: NodeJS.ErrnoException) => { console.error('FATAL: Server error', err.message); process.exit(1); }); setupSignalHandlers(async () => { httpServer.close(); }); } else { console.error(`Unknown transport: ${transport}. Use MCP_TRANSPORT=stdio or MCP_TRANSPORT=http`); process.exit(1); } })();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/domdomegg/gmail-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server