import { buildComputerUseTools, createComputerUseMcpServer, } from '@ant/computer-use-mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import { homedir } from 'os' import { shutdownDatadog } from '../../services/analytics/datadog.js' import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' import { initializeAnalyticsSink } from '../../services/analytics/sink.js' import { enableConfigs } from '../config.js' import { logForDebugging } from '../debug.js' import { filterAppsForDescription } from './appNames.js' import { getChicagoCoordinateMode } from './gates.js' import { getComputerUseHostAdapter } from './hostAdapter.js' const APP_ENUM_TIMEOUT_MS = 1000 /** * Enumerate installed apps, timed. Fails soft — if Spotlight is slow or * claude-swift throws, the tool description just omits the list. Resolution * happens at call time regardless; the model just doesn't get hints. */ async function tryGetInstalledAppNames(): Promise { const adapter = getComputerUseHostAdapter() const enumP = adapter.executor.listInstalledApps() let timer: ReturnType | undefined const timeoutP = new Promise(resolve => { timer = setTimeout(resolve, APP_ENUM_TIMEOUT_MS, undefined) }) const installed = await Promise.race([enumP, timeoutP]) .catch(() => undefined) .finally(() => clearTimeout(timer)) if (!installed) { // The enumeration continues in the background — swallow late rejections. void enumP.catch(() => {}) logForDebugging( `[Computer Use MCP] app enumeration exceeded ${APP_ENUM_TIMEOUT_MS}ms or failed; tool description omits list`, ) return undefined } return filterAppsForDescription(installed, homedir()) } /** * Construct the in-process server. Delegates to the package's * `createComputerUseMcpServer` for the Server object + stub CallTool handler, * then REPLACES the ListTools handler with one that includes installed-app * names in the `request_access` description (the package's factory doesn't * take `installedAppNames`, and Cowork builds its own tool array in * serverDef.ts for the same reason). * * Async so the 1s app-enumeration timeout doesn't block startup — called from * an `await import()` in `client.ts` on first CU connection, not `main.tsx`. * * Real dispatch still goes through `wrapper.tsx`'s `.call()` override; this * server exists only to answer ListTools. */ export async function createComputerUseMcpServerForCli(): Promise< ReturnType > { const adapter = getComputerUseHostAdapter() const coordinateMode = getChicagoCoordinateMode() const server = createComputerUseMcpServer(adapter, coordinateMode) const installedAppNames = await tryGetInstalledAppNames() const tools = buildComputerUseTools( adapter.executor.capabilities, coordinateMode, installedAppNames, ) server.setRequestHandler(ListToolsRequestSchema, async () => adapter.isDisabled() ? { tools: [] } : { tools }, ) return server } /** * Subprocess entrypoint for `--computer-use-mcp`. Mirror of * `runClaudeInChromeMcpServer` — stdio transport, exit on stdin close, * flush analytics before exit. */ export async function runComputerUseMcpServer(): Promise { enableConfigs() initializeAnalyticsSink() const server = await createComputerUseMcpServerForCli() const transport = new StdioServerTransport() let exiting = false const shutdownAndExit = async (): Promise => { if (exiting) return exiting = true await Promise.all([shutdown1PEventLogging(), shutdownDatadog()]) // eslint-disable-next-line custom-rules/no-process-exit process.exit(0) } process.stdin.on('end', () => void shutdownAndExit()) process.stdin.on('error', () => void shutdownAndExit()) logForDebugging('[Computer Use MCP] Starting MCP server') await server.connect(transport) logForDebugging('[Computer Use MCP] MCP server started') }