107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
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<string[] | undefined> {
|
|
const adapter = getComputerUseHostAdapter()
|
|
const enumP = adapter.executor.listInstalledApps()
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
const timeoutP = new Promise<undefined>(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<typeof createComputerUseMcpServer>
|
|
> {
|
|
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<void> {
|
|
enableConfigs()
|
|
initializeAnalyticsSink()
|
|
|
|
const server = await createComputerUseMcpServerForCli()
|
|
const transport = new StdioServerTransport()
|
|
|
|
let exiting = false
|
|
const shutdownAndExit = async (): Promise<void> => {
|
|
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')
|
|
}
|