use-mcp/test/integration/test-utils.ts
Jem e0743c35e4 chore: fix prettier formatting and update example server dependencies
- Fix formatting issues in cf-agents, hono-mcp, and test files
- Update dependencies in example servers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-02 17:59:24 -07:00

288 lines
8.2 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process'
import type { Page } from 'playwright'
import { readFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node: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
expectedResources?: number
expectedPrompts?: 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[]; resources: string[]; prompts: 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 {
// Look for the Available Tools section
const toolsSection = page.locator('h3:has-text("Available Tools")').locator('..')
const toolCards = toolsSection.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 available resources
const resources: string[] = []
try {
// Look for the Available Resources section
const resourcesSection = page.locator('h3:has-text("Available Resources")').locator('..')
const resourceCards = resourcesSection.locator('.bg-white.rounded.border')
const resourceCount = await resourceCards.count()
for (let i = 0; i < resourceCount; i++) {
const resourceNameElement = resourceCards.nth(i).locator('h4.font-medium.text-sm')
const resourceName = await resourceNameElement.textContent()
if (resourceName?.trim()) {
resources.push(resourceName.trim())
}
}
} catch (e) {
console.warn('Could not extract resources list:', e)
}
// Extract available prompts
const prompts: string[] = []
try {
// Look for the Available Prompts section
const promptsSection = page.locator('h3:has-text("Available Prompts")').locator('..')
const promptCards = promptsSection.locator('.bg-white.rounded.border')
const promptCount = await promptCards.count()
for (let i = 0; i < promptCount; i++) {
const promptNameElement = promptCards.nth(i).locator('h4.font-medium.text-sm')
const promptName = await promptNameElement.textContent()
if (promptName?.trim()) {
prompts.push(promptName.trim())
}
}
} catch (e) {
console.warn('Could not extract prompts 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,
resources,
prompts,
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
}
}