244 lines
6.5 KiB
TypeScript
244 lines
6.5 KiB
TypeScript
import { spawn, ChildProcess } from 'child_process'
|
|
import { Page } from 'playwright'
|
|
import { readFileSync } from 'fs'
|
|
import { join, dirname } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const testDir = join(__dirname, '..')
|
|
const testStateFile = join(testDir, 'node_modules/.cache/use-mcp-tests/test-state.json')
|
|
|
|
export interface TestState {
|
|
honoPort?: number
|
|
cfAgentsPort?: number
|
|
staticPort?: number
|
|
}
|
|
|
|
export interface ServerEndpoint {
|
|
path: string
|
|
transportTypes: ('auto' | 'http' | 'sse')[]
|
|
}
|
|
|
|
export interface ServerConfig {
|
|
name: string
|
|
directory: string
|
|
portKey: keyof TestState
|
|
endpoints: ServerEndpoint[]
|
|
expectedTools: number
|
|
}
|
|
|
|
/**
|
|
* Read the test state file containing server ports
|
|
*/
|
|
export function getTestState(): TestState {
|
|
try {
|
|
const stateData = readFileSync(testStateFile, 'utf-8')
|
|
return JSON.parse(stateData)
|
|
} catch (error) {
|
|
throw new Error(`Test environment not properly initialized: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for a process to output a specific string
|
|
*/
|
|
export function waitForOutput(process: ChildProcess, targetOutput: string, timeout = 30000): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new Error(`Timeout waiting for: ${targetOutput}`))
|
|
}, timeout)
|
|
|
|
const onData = (data: Buffer) => {
|
|
const output = data.toString()
|
|
console.log(`[server output] ${output}`)
|
|
if (output.includes(targetOutput)) {
|
|
clearTimeout(timer)
|
|
process.stdout?.off('data', onData)
|
|
process.stderr?.off('data', onData)
|
|
resolve()
|
|
}
|
|
}
|
|
|
|
process.stdout?.on('data', onData)
|
|
process.stderr?.on('data', onData)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Check if a port is available
|
|
*/
|
|
export async function checkPortAvailable(port: number): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const server = require('net').createServer()
|
|
|
|
server.listen(port, () => {
|
|
server.close(() => {
|
|
setTimeout(() => resolve(true), 100)
|
|
})
|
|
})
|
|
|
|
server.on('error', (err: any) => {
|
|
console.log(`Port ${port} check failed: ${err.message}`)
|
|
resolve(false)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Find an available port starting from a base port
|
|
*/
|
|
export function findAvailablePortFromBase(basePort: number): Promise<number> {
|
|
return new Promise(async (resolve, reject) => {
|
|
for (let port = basePort; port < basePort + 20; port++) {
|
|
if (await checkPortAvailable(port)) {
|
|
resolve(port)
|
|
return
|
|
}
|
|
}
|
|
reject(new Error(`No available ports found starting from ${basePort}`))
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Spawn a server process in development mode
|
|
*/
|
|
export function spawnDevServer(serverName: string, directory: string, port: number, onOutput?: (data: string) => void): ChildProcess {
|
|
const server = spawn('pnpm', ['dev', `--port=${port}`], {
|
|
cwd: directory,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
shell: true,
|
|
detached: false,
|
|
})
|
|
|
|
server.stdout?.on('data', (data) => {
|
|
const output = `[${serverName}] ${data.toString()}`
|
|
console.log(output)
|
|
onOutput?.(output)
|
|
})
|
|
|
|
server.stderr?.on('data', (data) => {
|
|
const output = `[${serverName}] ${data.toString()}`
|
|
console.log(output)
|
|
onOutput?.(output)
|
|
})
|
|
|
|
return server
|
|
}
|
|
|
|
/**
|
|
* Connect to an MCP server through the inspector UI
|
|
*/
|
|
export async function connectToMCPServer(
|
|
page: Page,
|
|
serverUrl: string,
|
|
transportType: 'auto' | 'http' | 'sse' = 'auto',
|
|
): Promise<{ success: boolean; tools: string[]; debugLog: string }> {
|
|
const state = getTestState()
|
|
|
|
if (!state.staticPort) {
|
|
throw new Error('Static server port not available - state: ' + JSON.stringify(state))
|
|
}
|
|
|
|
await page.goto(`http://localhost:${state.staticPort}`)
|
|
await page.waitForSelector('input[placeholder="Enter MCP server URL"]', { timeout: 10000 })
|
|
|
|
// Enter the server URL
|
|
const urlInput = page.locator('input[placeholder="Enter MCP server URL"]')
|
|
await urlInput.fill(serverUrl)
|
|
|
|
// Set transport type
|
|
const transportSelect = page.locator('select')
|
|
await transportSelect.selectOption(transportType)
|
|
|
|
// Click connect button
|
|
const connectButton = page.locator('button:has-text("Connect")')
|
|
await connectButton.click()
|
|
|
|
// Wait for connection attempt to complete
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Check for connection status
|
|
let attempts = 0
|
|
const maxAttempts = 20
|
|
let isConnected = false
|
|
|
|
while (attempts < maxAttempts && !isConnected) {
|
|
try {
|
|
// Check if status badge shows "Connected"
|
|
const statusBadge = page.locator('.px-2.py-1.rounded-full')
|
|
if ((await statusBadge.count()) > 0) {
|
|
const statusText = await statusBadge.textContent({ timeout: 500 })
|
|
if (statusText?.toLowerCase().includes('connected')) {
|
|
isConnected = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Also check if tools count is > 0
|
|
const toolsHeader = page.locator('h3:has-text("Available Tools")')
|
|
if ((await toolsHeader.count()) > 0) {
|
|
const toolsText = await toolsHeader.textContent()
|
|
if (toolsText && /\d+/.test(toolsText)) {
|
|
const toolsCount = parseInt(toolsText.match(/\d+/)?.[0] || '0')
|
|
if (toolsCount > 0) {
|
|
isConnected = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Continue waiting
|
|
}
|
|
|
|
await page.waitForTimeout(500)
|
|
attempts++
|
|
}
|
|
|
|
// Extract available tools
|
|
const tools: string[] = []
|
|
try {
|
|
const toolCards = page.locator('.bg-white.rounded.border')
|
|
const toolCount = await toolCards.count()
|
|
|
|
for (let i = 0; i < toolCount; i++) {
|
|
const toolNameElement = toolCards.nth(i).locator('h4.font-bold.text-base.text-black')
|
|
const toolName = await toolNameElement.textContent()
|
|
if (toolName?.trim()) {
|
|
tools.push(toolName.trim())
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Could not extract tools list:', e)
|
|
}
|
|
|
|
// Extract debug log
|
|
let debugLog = ''
|
|
try {
|
|
const debugContainer = page.locator('.h-32.overflow-y-auto.font-mono.text-xs')
|
|
if ((await debugContainer.count()) > 0) {
|
|
debugLog = (await debugContainer.first().textContent()) || ''
|
|
}
|
|
} catch (e) {
|
|
console.warn('Could not extract debug log:', e)
|
|
}
|
|
|
|
return {
|
|
success: isConnected,
|
|
tools,
|
|
debugLog,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up a child process safely
|
|
*/
|
|
export function cleanupProcess(process: ChildProcess, name: string): void {
|
|
try {
|
|
if (process && !process.killed) {
|
|
console.log(`🔥 Cleaning up ${name} server...`)
|
|
process.kill('SIGKILL')
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors - process might already be dead
|
|
}
|
|
}
|