Skip to main content
Glama
auth-coordinator.ts8.85 kB
import type * as http from 'node:http' import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' import { auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import type { OAuthClientInformation } from '@modelcontextprotocol/sdk/shared/auth.js' import type { OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js' import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' import type { Request, Response } from 'express' import express from 'express' import type { ConfigRepository } from './config-repository.js' import type { InvalidateCredentialsScope } from './node-oauth-client-provider.js' import { NodeOauthClientProvider } from './node-oauth-client-provider.js' import { debugLog, log, setupShutdownHook, sleep } from './utils.js' interface LockData { pid: number expiresAt: Date } enum TransportType { StreamableHTTP = 'streamable-http', SSE = 'sse', } export class AuthCoordinator { private server: http.Server | undefined = undefined constructor(private readonly configRepository: ConfigRepository) {} public async startCallbackServer() { return new Promise<() => unknown>(async (resolve, reject) => { const app = express() app.get('/auth/callback', async (req: Request, res: Response) => { const sendResponse = (status: number, message: string) => { res.status(status).send(`<h1>${message}</h1><p>You can now close this window</p>`) } try { const code = req.query.code const state = req.query.state if (typeof code !== 'string') { return sendResponse(400, 'Invalid authorization code') } if (typeof state !== 'string') { return sendResponse(400, 'Invalid state parameter') } if (decodeURIComponent(state) !== this.getServerUrl().toString()) { return sendResponse(400, 'Wrong authorization server') } sendResponse(200, 'Authorization Completed') log('Authorization code received') await auth(this.getAuthProvider(), { serverUrl: this.configRepository.getServerUrl(), authorizationCode: code, }) log('Authentication completed successfully') } catch (error) { log('Error handling auth callback:', error) return sendResponse(500, 'Internal Server Error') } }) const serverPort = await this.configRepository.readConfig<number>('auth-server-port') this.server = app.listen(serverPort || 0, (err) => { if (err) { if ((err as any).code === 'EADDRINUSE') { log(`Authentication server port ${serverPort} is already in use. Probably mcp proxy is already running.`) return resolve(() => {}) } else { log('Error starting authentication server:', err) return reject(err) } } const address = this.server?.address() if (!address || typeof address === 'string') { log('Failed to start authentication server: Port is not available') return reject(new Error('Authentication server address is not available')) } log(`Authentication server started successfully on http://localhost:${address.port}`) return resolve(() => { if (this.server) { this.server.close() this.server = undefined } }) }) }) } public async initRemoteTransport(apiKey?: string): Promise<Transport> { let lockFileCreated = false setupShutdownHook(async () => { if (lockFileCreated) { await this.configRepository.deleteConfig('lock') } }) const availableTransports = Object.values(TransportType) let currentTransportIndex = 0 while (true) { const lockData = await this.configRepository.readConfig<LockData>('lock') if (!lockData || (lockData.expiresAt < new Date() && lockData.pid !== process.pid)) { log('Test remote authentication') await this.configRepository.writeConfig<LockData>('lock', { pid: process.pid, expiresAt: new Date(Date.now() + 1000 * 60 * 10), }) lockFileCreated = true try { const testTransport = this.createRemoteTransport(availableTransports[currentTransportIndex], apiKey) const testClient = new Client( { name: 'sequa-mcp-authentication-test', version: '1.0.0' }, { capabilities: {} }, ) await testClient.connect(testTransport) await testClient.close() await testTransport.close() lockFileCreated = false await this.configRepository.deleteConfig('lock') break } catch (error) { if (error instanceof UnauthorizedError) { continue } lockFileCreated = false await this.configRepository.deleteConfig('lock') if (currentTransportIndex < availableTransports.length - 1) { debugLog(`Error with transport ${availableTransports[currentTransportIndex]}:`, error) currentTransportIndex++ log(`Switching to next transport: ${availableTransports[currentTransportIndex]}`) continue } throw error } } debugLog('Waiting for tokens...') await sleep(2000) } return this.createRemoteTransport(availableTransports[currentTransportIndex], apiKey) } public getServerUrl(): URL { return this.configRepository.getServerUrl() } public getCallbackPort(): number { const address = this.server?.address() if (!address || typeof address === 'string') { throw new Error('Redirect URL cannot be retrieved before server is started') } return address.port } public getRedirectUrl(): string { return `http://localhost:${this.getCallbackPort()}/auth/callback` } public async getClientInformation(): Promise<OAuthClientInformation | undefined> { return await this.configRepository.readConfig<OAuthClientInformation>('client-information') } public async saveClientInformation(clientInformation: OAuthClientInformation): Promise<void> { await this.configRepository.writeConfig<number>('auth-server-port', this.getCallbackPort()) await this.configRepository.writeConfig<OAuthClientInformation>('client-information', clientInformation) } public async getTokens(): Promise<OAuthTokens | undefined> { return await this.configRepository.readConfig<OAuthTokens>('tokens') } public async saveTokens(tokens: OAuthTokens): Promise<void> { await this.configRepository.writeConfig<OAuthTokens>('tokens', tokens) await this.configRepository.deleteConfig('lock') await this.configRepository.deleteConfig('code-verifier') } public async getCodeVerifier(): Promise<string | undefined> { return await this.configRepository.readConfig<string>('code-verifier') } public async saveCodeVerifier(codeVerifier: string): Promise<void> { await this.configRepository.writeConfig<string>('code-verifier', codeVerifier) } private createRemoteTransport(type: TransportType, apiKey?: string): Transport { const customFetch = (url: string | URL, init?: RequestInit) => { if (apiKey) { const headers = new Headers(init?.headers) headers.set('X-Api-Key', apiKey) return fetch(url, init ? { ...init, headers } : { headers }) } return fetch(url, init) } if (type === TransportType.StreamableHTTP) { return new StreamableHTTPClientTransport(this.configRepository.getServerUrl(), { authProvider: this.getAuthProvider(), fetch: customFetch, }) } else if (type === TransportType.SSE) { return new SSEClientTransport(this.configRepository.getServerUrl(), { authProvider: this.getAuthProvider(), fetch: customFetch, }) } throw new Error(`Unsupported transport type: ${type}`) } private getAuthProvider(): OAuthClientProvider { return new NodeOauthClientProvider(this) } async invalidateCredentials(scope: InvalidateCredentialsScope) { if (scope === 'all' || scope === 'client') { await this.configRepository.deleteConfig('client-information') } if (scope === 'all' || scope === 'tokens') { await this.configRepository.deleteConfig('tokens') } if (scope === 'all' || scope === 'verifier') { await this.configRepository.deleteConfig('code-verifier') } } }

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/sequa-ai/sequa-mcp'

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