/ utils / computerUse / mcpServer.ts
mcpServer.ts
  1  import {
  2    buildComputerUseTools,
  3    createComputerUseMcpServer,
  4  } from '@ant/computer-use-mcp'
  5  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
  6  import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
  7  import { homedir } from 'os'
  8  
  9  import { shutdownDatadog } from '../../services/analytics/datadog.js'
 10  import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js'
 11  import { initializeAnalyticsSink } from '../../services/analytics/sink.js'
 12  import { enableConfigs } from '../config.js'
 13  import { logForDebugging } from '../debug.js'
 14  import { filterAppsForDescription } from './appNames.js'
 15  import { getChicagoCoordinateMode } from './gates.js'
 16  import { getComputerUseHostAdapter } from './hostAdapter.js'
 17  
 18  const APP_ENUM_TIMEOUT_MS = 1000
 19  
 20  /**
 21   * Enumerate installed apps, timed. Fails soft — if Spotlight is slow or
 22   * claude-swift throws, the tool description just omits the list. Resolution
 23   * happens at call time regardless; the model just doesn't get hints.
 24   */
 25  async function tryGetInstalledAppNames(): Promise<string[] | undefined> {
 26    const adapter = getComputerUseHostAdapter()
 27    const enumP = adapter.executor.listInstalledApps()
 28    let timer: ReturnType<typeof setTimeout> | undefined
 29    const timeoutP = new Promise<undefined>(resolve => {
 30      timer = setTimeout(resolve, APP_ENUM_TIMEOUT_MS, undefined)
 31    })
 32    const installed = await Promise.race([enumP, timeoutP])
 33      .catch(() => undefined)
 34      .finally(() => clearTimeout(timer))
 35    if (!installed) {
 36      // The enumeration continues in the background — swallow late rejections.
 37      void enumP.catch(() => {})
 38      logForDebugging(
 39        `[Computer Use MCP] app enumeration exceeded ${APP_ENUM_TIMEOUT_MS}ms or failed; tool description omits list`,
 40      )
 41      return undefined
 42    }
 43    return filterAppsForDescription(installed, homedir())
 44  }
 45  
 46  /**
 47   * Construct the in-process server. Delegates to the package's
 48   * `createComputerUseMcpServer` for the Server object + stub CallTool handler,
 49   * then REPLACES the ListTools handler with one that includes installed-app
 50   * names in the `request_access` description (the package's factory doesn't
 51   * take `installedAppNames`, and Cowork builds its own tool array in
 52   * serverDef.ts for the same reason).
 53   *
 54   * Async so the 1s app-enumeration timeout doesn't block startup — called from
 55   * an `await import()` in `client.ts` on first CU connection, not `main.tsx`.
 56   *
 57   * Real dispatch still goes through `wrapper.tsx`'s `.call()` override; this
 58   * server exists only to answer ListTools.
 59   */
 60  export async function createComputerUseMcpServerForCli(): Promise<
 61    ReturnType<typeof createComputerUseMcpServer>
 62  > {
 63    const adapter = getComputerUseHostAdapter()
 64    const coordinateMode = getChicagoCoordinateMode()
 65    const server = createComputerUseMcpServer(adapter, coordinateMode)
 66  
 67    const installedAppNames = await tryGetInstalledAppNames()
 68    const tools = buildComputerUseTools(
 69      adapter.executor.capabilities,
 70      coordinateMode,
 71      installedAppNames,
 72    )
 73    server.setRequestHandler(ListToolsRequestSchema, async () =>
 74      adapter.isDisabled() ? { tools: [] } : { tools },
 75    )
 76  
 77    return server
 78  }
 79  
 80  /**
 81   * Subprocess entrypoint for `--computer-use-mcp`. Mirror of
 82   * `runClaudeInChromeMcpServer` — stdio transport, exit on stdin close,
 83   * flush analytics before exit.
 84   */
 85  export async function runComputerUseMcpServer(): Promise<void> {
 86    enableConfigs()
 87    initializeAnalyticsSink()
 88  
 89    const server = await createComputerUseMcpServerForCli()
 90    const transport = new StdioServerTransport()
 91  
 92    let exiting = false
 93    const shutdownAndExit = async (): Promise<void> => {
 94      if (exiting) return
 95      exiting = true
 96      await Promise.all([shutdown1PEventLogging(), shutdownDatadog()])
 97      // eslint-disable-next-line custom-rules/no-process-exit
 98      process.exit(0)
 99    }
100    process.stdin.on('end', () => void shutdownAndExit())
101    process.stdin.on('error', () => void shutdownAndExit())
102  
103    logForDebugging('[Computer Use MCP] Starting MCP server')
104    await server.connect(transport)
105    logForDebugging('[Computer Use MCP] MCP server started')
106  }