Skip to main content
Glama

MCP Auth

by rubenpenap
globalSetup.ts7.88 kB
import { execa } from 'execa' import getPort from 'get-port' import { type TestProject } from 'vitest/node' declare module 'vitest' { export interface ProvidedContext { mcpServerPort: number } } type OutputBuffer = Array<{ channel: 'stdout' | 'stderr'; data: Buffer }> export default async function setup(project: TestProject) { const mcpServerPort = process.env.PORT ? Number(process.env.PORT) : await getPort() project.provide('mcpServerPort', mcpServerPort) let appServerProcess: ReturnType<typeof execa> | null = null let mcpServerProcess: ReturnType<typeof execa> | null = null // Buffers to store output for potential error display const appServerOutput: OutputBuffer = [] const mcpServerOutput: OutputBuffer = [] /** * Wait for a server to be ready by monitoring its output for a specific text pattern */ async function waitForServerReady({ process: childProcess, textMatch, name, outputBuffer, }: { process: ReturnType<typeof execa> | null textMatch: string name: string outputBuffer: OutputBuffer }) { if (!childProcess) return return new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => { childProcess?.kill() reject(new Error(`${name} failed to start within 10 seconds`)) }, 10_000) function searchForMatch( channel: OutputBuffer[number]['channel'], data: Buffer, ) { outputBuffer.push({ channel, data }) const str = data.toString() if (str.includes(textMatch)) { clearTimeout(timeout) // Remove the listeners after finding the match childProcess?.stdout?.removeListener('data', searchForMatch) childProcess?.stderr?.removeListener('data', searchForMatch) resolve() } } childProcess?.stdout?.on('data', searchForMatch.bind(null, 'stdout')) childProcess?.stderr?.on('data', searchForMatch.bind(null, 'stderr')) childProcess?.on('error', (err) => { clearTimeout(timeout) reject(err) }) childProcess?.on('exit', (code) => { if (code !== 0) { clearTimeout(timeout) reject(new Error(`${name} exited with code ${code}`)) } }) }) } /** * Display buffered output when there's a failure */ function displayBufferedOutput() { if (appServerOutput.length > 0) { console.log('=== App Server Output ===') for (const { channel, data } of appServerOutput) { process[channel].write(data) } } if (mcpServerOutput.length > 0) { console.log('=== MCP Server Output ===') for (const { channel, data } of mcpServerOutput) { process[channel].write(data) } } } async function startAppServerIfNecessary() { const isAppRunning = await fetch('http://localhost:7788/healthcheck').catch( () => ({ ok: false }), ) if (isAppRunning.ok) { return } const rootDir = process.cwd().replace(/exercises\/.*$/, '') // Start the app server from the root directory console.log(`Starting app server on port 7788...`) const command = 'npm' // prettier-ignore const args = [ 'run', 'dev', '--prefix', './epicshop/epic-me', '--', '--clearScreen=false', '--strictPort', ] appServerProcess = execa(command, args, { cwd: rootDir, stdio: ['ignore', 'pipe', 'pipe'], }) } async function startMcpServerIfNecessary() { const isMcpRunning = await fetch( `http://localhost:${mcpServerPort}/healthcheck`, ).catch(() => ({ ok: false })) if (isMcpRunning.ok) { return } // Start the MCP server if necessary console.log(`Starting MCP server on port ${mcpServerPort}...`) mcpServerProcess = execa( 'npx', ['wrangler', 'dev', '--port', mcpServerPort.toString()], { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PORT: mcpServerPort.toString(), }, }, ) } async function startServers() { console.log('Starting servers...') // Start app server if necessary await startAppServerIfNecessary() // Start MCP server if necessary await startMcpServerIfNecessary() try { // Wait for both servers to be ready simultaneously await Promise.all([ appServerProcess ? waitForServerReady({ process: appServerProcess, textMatch: '7788', name: '[APP-SERVER]', outputBuffer: appServerOutput, }).then(() => waitForResourceReady('http://localhost:7788/healthcheck'), ) : Promise.resolve(), waitForServerReady({ process: mcpServerProcess, textMatch: mcpServerPort.toString(), name: '[MCP-SERVER]', outputBuffer: mcpServerOutput, }).then(() => waitForResourceReady(`http://localhost:${mcpServerPort}/healthcheck`), ), ]) console.log('Servers started successfully') } catch (error) { // Display buffered output on failure displayBufferedOutput() throw error } } async function cleanup() { console.log('Cleaning up servers...') const cleanupPromises: Array<Promise<void>> = [] if (mcpServerProcess && !mcpServerProcess.killed) { cleanupPromises.push( (async () => { mcpServerProcess.kill('SIGTERM') // Give it time to gracefully shutdown, then force kill const timeout = setTimeout(() => { if (mcpServerProcess && !mcpServerProcess.killed) { mcpServerProcess.kill('SIGKILL') } }, 500) try { await mcpServerProcess } catch { // Process was killed, which is expected } finally { clearTimeout(timeout) } })(), ) } if (appServerProcess && !appServerProcess.killed) { cleanupPromises.push( (async () => { appServerProcess.kill('SIGTERM') // Give time to gracefully shutdown, then force kill const timeout = setTimeout(() => { if (appServerProcess && !appServerProcess.killed) { appServerProcess.kill('SIGKILL') } }, 500) try { await appServerProcess } catch { // Process was killed, which is expected } finally { clearTimeout(timeout) } })(), ) } // Wait for all cleanup to complete, but with an overall timeout try { await Promise.race([ Promise.all(cleanupPromises), new Promise((_, reject) => setTimeout(() => reject(new Error('Cleanup timeout')), 5000), ), ]) } catch (error) { console.warn( 'Cleanup warning:', error instanceof Error ? error.message : String(error), ) // Force kill any remaining processes if (mcpServerProcess && !mcpServerProcess.killed) { mcpServerProcess.kill('SIGKILL') } if (appServerProcess && !appServerProcess.killed) { appServerProcess.kill('SIGKILL') } } console.log('Servers cleaned up') } // Start servers and wait for them to be ready before returning await startServers() // Return cleanup function return cleanup } function waitForResourceReady(resourceUrl: string) { const timeoutSignal = AbortSignal.timeout(10_000) let lastError: Error | null = null return new Promise<void>((resolve, reject) => { timeoutSignal.addEventListener('abort', () => { const error = lastError ?? new Error('No other errors detected') error.message = `Timed out waiting for ${resourceUrl}:\n Last Error:${error.message}` reject(error) }) async function checkResource() { try { const response = await fetch(resourceUrl) if (response.ok) return resolve() } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)) } await sleep(100, timeoutSignal) await checkResource() } return checkResource() }) } function sleep(ms: number, signal?: AbortSignal) { return new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => { signal?.removeEventListener('abort', onAbort) resolve() }, ms) function onAbort() { clearTimeout(timeout) reject(new Error('Sleep aborted')) } signal?.addEventListener('abort', onAbort) }) }

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/rubenpenap/mcp-auth'

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