mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-22 05:15:13 +00:00
git-subtree-dir: packages/browseros-agent git-subtree-mainline:8f148d0918git-subtree-split:90bd4be300
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import { execSync } from 'node:child_process'
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
|
|
import { Mutex } from 'async-mutex'
|
|
|
|
import { ensureBrowserOS } from './setup'
|
|
|
|
// =============================================================================
|
|
// Port Management
|
|
// =============================================================================
|
|
|
|
export async function killProcessOnPort(port: number): Promise<void> {
|
|
try {
|
|
console.log(`Finding process on port ${port}...`)
|
|
|
|
const pids = execSync(`lsof -ti :${port}`, {
|
|
encoding: 'utf-8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
}).trim()
|
|
|
|
if (pids) {
|
|
const pidList = pids.replace(/\n/g, ', ')
|
|
console.log(`Terminating process(es) ${pidList} on port ${port}...`)
|
|
|
|
try {
|
|
execSync(`kill -15 ${pids.replace(/\n/g, ' ')}`, {
|
|
stdio: 'ignore',
|
|
})
|
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
} catch {
|
|
execSync(`kill -9 ${pids.replace(/\n/g, ' ')}`, {
|
|
stdio: 'ignore',
|
|
})
|
|
}
|
|
|
|
console.log(`Terminated process on port ${port}`)
|
|
}
|
|
} catch {
|
|
console.log(`No process found on port ${port}`)
|
|
}
|
|
|
|
console.log('Waiting 1 second for port to be released...')
|
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test Wrappers
|
|
// =============================================================================
|
|
|
|
const envMutex = new Mutex()
|
|
|
|
export async function withMcpServer(
|
|
cb: (client: Client) => Promise<void>,
|
|
): Promise<void> {
|
|
return await envMutex.runExclusive(async () => {
|
|
const config = await ensureBrowserOS()
|
|
|
|
const client = new Client({
|
|
name: 'browseros-test-client',
|
|
version: '1.0.0',
|
|
})
|
|
|
|
const serverUrl = new URL(`http://127.0.0.1:${config.serverPort}/mcp`)
|
|
const transport = new StreamableHTTPClientTransport(serverUrl)
|
|
|
|
try {
|
|
await client.connect(transport)
|
|
await cb(client)
|
|
} finally {
|
|
await transport.close()
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// HTML Helper
|
|
// =============================================================================
|
|
|
|
export function html(
|
|
strings: TemplateStringsArray,
|
|
...values: unknown[]
|
|
): string {
|
|
const bodyContent = strings.reduce((acc, str, i) => {
|
|
return acc + str + (values[i] || '')
|
|
}, '')
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>My test page</title>
|
|
</head>
|
|
<body>
|
|
${bodyContent}
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
// =============================================================================
|
|
// Type Helpers
|
|
// =============================================================================
|
|
|
|
export interface McpContentItem {
|
|
type: 'text' | 'image'
|
|
text?: string
|
|
data?: string
|
|
mimeType?: string
|
|
}
|
|
|
|
export interface TypedCallToolResult {
|
|
content: McpContentItem[]
|
|
isError?: boolean
|
|
}
|
|
|
|
export function asToolResult(result: CallToolResult): TypedCallToolResult {
|
|
return result as unknown as TypedCallToolResult
|
|
}
|