Files
moeka-project/services/computer-use-mcp/src/server/register-tools.ts
T
2026-04-29 22:51:19 +08:00

1307 lines
45 KiB
TypeScript

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import type {
BrowserDomFrameResult,
ClickActionInput,
FocusAppActionInput,
OpenAppActionInput,
SecretReadEnvValueActionInput,
TerminalExecActionInput,
TypeTextActionInput,
} from '../types'
import type { WorkflowSuspension } from '../workflows'
import type { ExecuteAction } from './action-executor'
import type { ComputerUseServerRuntime } from './runtime'
import { z } from 'zod'
import { getUnsupportedBrowserDomActions, isBrowserDomActionSupported } from '../browser-dom/capabilities'
import { getRuntimePreflight } from '../preflight'
import { summarizeRunState } from '../transparency'
import {
createAppBrowseAndActWorkflow,
createDevInspectFailureWorkflow,
createDevOpenWorkspaceWorkflow,
createDevRunTestsWorkflow,
createDevValidateWorkspaceWorkflow,
executeWorkflow,
resumeWorkflow,
} from '../workflows'
import { getBrowserAgentLaunchContext, runBrowserAgentTask } from './browser-agent'
import { textContent } from './content'
import {
describeExecutionTarget,
describeForegroundContext,
summarizeCoordinateSpace,
} from './formatters'
import { refreshRuntimeRunState } from './refresh-run-state'
import { executeChromeEnsure } from './register-chrome-session'
import { createAcquirePtyCallback, executeApprovedPtyCreate } from './register-pty'
import { formatWorkflowStructuredContent } from './workflow-formatter'
import { createWorkflowPrepToolExecutor } from './workflow-prep-tools'
export interface RegisterComputerUseToolsOptions {
server: McpServer
runtime: ComputerUseServerRuntime
executeAction: ExecuteAction
enableTestTools: boolean
}
const optionalTabIdSchema = z.number().int().min(0).optional().describe('Optional browser tab id override; defaults to the active tab')
const optionalFrameIdsSchema = z.array(z.number().int().min(0)).min(1).optional().describe('Optional frame ids to target; omit to let the bridge inspect all frames')
function toBrowserDomRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value))
return undefined
return value as Record<string, unknown>
}
function unwrapBrowserDomResult(value: unknown) {
const record = toBrowserDomRecord(value)
if (!record)
return value
if ('data' in record)
return record.data
return value
}
function didBrowserDomFrameSucceed(frame: BrowserDomFrameResult<unknown>) {
const record = toBrowserDomRecord(frame.result)
if (!record)
return Boolean(frame.result)
if ('success' in record)
return Boolean(record.success)
return true
}
function summarizeBrowserDomFrameResults(label: string, results: Array<BrowserDomFrameResult<unknown>>) {
const successfulFrames = results.filter(didBrowserDomFrameSucceed)
return `${label}: ${successfulFrames.length}/${results.length} frame(s) succeeded.`
}
function buildBrowserDomUnavailableResponse(runtime: ComputerUseServerRuntime, unsupportedActions?: string[]) {
const status = runtime.browserDomBridge.getStatus()
const detail = unsupportedActions?.length
? `connected extension transport does not support ${unsupportedActions.join(', ')}`
: status.lastError || 'the browser extension is not connected yet'
return {
isError: true,
content: [
textContent(`Browser DOM bridge is unavailable: ${detail}.`),
],
structuredContent: {
status: 'unavailable',
bridge: status,
unsupportedActions,
},
}
}
export function registerComputerUseTools(params: RegisterComputerUseToolsOptions) {
const { server, runtime, executeAction, enableTestTools } = params
const executePrepTool = createWorkflowPrepToolExecutor(runtime)
const acquirePty = createAcquirePtyCallback(runtime)
async function refreshWorkflowRunState() {
await refreshRuntimeRunState(runtime)
}
// Workflow suspension state — stored in this closure so that
// workflow_resume and the approve handler can access it.
let suspendedWorkflow: WorkflowSuspension | undefined
server.tool(
'desktop_get_capabilities',
{},
async () => {
const [{ executionTarget, context, displayInfo, browserSurfaceAvailability }, permissionInfo] = await Promise.all([
refreshRuntimeRunState(runtime),
runtime.executor.getPermissionInfo(),
])
const snapshot = runtime.session.getSnapshot()
const preflight = getRuntimePreflight({
config: runtime.config,
lastScreenshot: runtime.session.getLastScreenshot(),
displayInfo,
executionTarget,
})
return {
content: [
textContent(
`Executor=${runtime.config.executor}, host=${preflight.launchContext.hostName}, target=${describeExecutionTarget(executionTarget)}, sessionTag=${preflight.launchContext.sessionTag || 'missing'}, coordinateSpace=${summarizeCoordinateSpace(preflight.coordinateSpace)}. Foreground=${describeForegroundContext(context)}.`,
),
],
structuredContent: {
executor: runtime.executor.describe(),
launchContext: preflight.launchContext,
executionTarget,
displayInfo,
permissions: permissionInfo,
coordinateSpace: preflight.coordinateSpace,
mutationGuards: {
applies: runtime.config.executor !== 'dry-run',
requireSessionTagForMutatingActions: runtime.config.requireSessionTagForMutatingActions,
requireAllowedBoundsForMutatingActions: runtime.config.requireAllowedBoundsForMutatingActions,
requireCoordinateAlignmentForMutatingActions: runtime.config.requireCoordinateAlignmentForMutatingActions,
readyForMutations: preflight.mutationReadinessIssues.length === 0,
blockingIssues: preflight.mutationReadinessIssues,
},
policy: {
approvalMode: runtime.config.approvalMode,
allowedBounds: runtime.config.allowedBounds,
allowApps: runtime.config.allowApps,
denyApps: runtime.config.denyApps,
denyWindowTitles: runtime.config.denyWindowTitles,
openableApps: runtime.config.openableApps,
maxOperations: runtime.config.maxOperations,
maxOperationUnits: runtime.config.maxOperationUnits,
defaultCaptureAfter: runtime.config.defaultCaptureAfter,
},
session: snapshot,
foregroundContext: context,
windowAutomation: runtime.config.executor === 'macos-local'
? 'NSWorkspace + CGWindowList + Quartz'
: runtime.config.executor === 'linux-x11'
? 'remote X11 runner'
: 'dry-run',
supportedAppsForOpenFocus: runtime.config.openableApps,
approvalUx: 'electron-dialog',
coordScope: 'global-screen',
appPolicy: 'deny-only',
terminalBackend: runtime.terminalRunner.describe().kind,
browserAgent: getBrowserAgentLaunchContext(),
browserDomBridge: runtime.browserDomBridge.getStatus(),
browserSurfaceAvailability,
},
}
},
)
if (enableTestTools && runtime.executor.openTestTarget) {
server.tool(
'desktop_open_test_target',
{},
async () => {
const result = await runtime.executor.openTestTarget!()
return {
content: [
textContent(`Opened ${result.appName} on ${describeExecutionTarget(result.executionTarget)}.`),
],
structuredContent: {
status: 'executed',
appName: result.appName,
windowTitle: result.windowTitle,
recommendedClickPoint: result.recommendedClickPoint,
executionTarget: result.executionTarget,
},
}
},
)
}
server.tool(
'desktop_observe_windows',
{
limit: z.number().int().min(1).max(32).optional().describe('Maximum number of visible windows to return'),
app: z.string().optional().describe('Optional app-name substring filter'),
},
async input => executeAction({ kind: 'observe_windows', input }, 'desktop_observe_windows'),
)
server.tool(
'desktop_screenshot',
{
label: z.string().optional().describe('Optional label for the saved screenshot file'),
},
async ({ label }) => executeAction({ kind: 'screenshot', input: { label } }, 'desktop_screenshot'),
)
server.tool(
'desktop_open_app',
{
app: z.string().min(1).describe('Application name from COMPUTER_USE_OPENABLE_APPS'),
},
async (input: OpenAppActionInput) => executeAction({ kind: 'open_app', input }, 'desktop_open_app'),
)
server.tool(
'desktop_focus_app',
{
app: z.string().min(1).describe('Application name from COMPUTER_USE_OPENABLE_APPS'),
},
async (input: FocusAppActionInput) => executeAction({ kind: 'focus_app', input }, 'desktop_focus_app'),
)
server.tool(
'desktop_click',
{
x: z.number().describe('Global logical screen X coordinate, not Retina backing pixels'),
y: z.number().describe('Global logical screen Y coordinate, not Retina backing pixels'),
button: z.enum(['left', 'right', 'middle']).optional().describe('Mouse button, default left'),
clickCount: z.number().int().min(1).max(2).optional().describe('Number of clicks, default 1'),
captureAfter: z.boolean().optional().describe('Whether to return a fresh screenshot after the action'),
},
async (input: ClickActionInput) => executeAction({ kind: 'click', input }, 'desktop_click'),
)
server.tool(
'desktop_type_text',
{
text: z.string().min(1).describe('Text to type into the focused UI element'),
x: z.number().optional().describe('Optional global logical screen X coordinate to click before typing'),
y: z.number().optional().describe('Optional global logical screen Y coordinate to click before typing'),
pressEnter: z.boolean().optional().describe('Whether to press Enter after typing'),
captureAfter: z.boolean().optional().describe('Whether to return a fresh screenshot after the action'),
},
async (input: TypeTextActionInput) => executeAction({ kind: 'type_text', input }, 'desktop_type_text'),
)
server.tool(
'desktop_press_keys',
{
keys: z.array(z.string()).min(1).describe('Single key chord, e.g. ["ctrl", "l"]'),
captureAfter: z.boolean().optional().describe('Whether to return a fresh screenshot after the action'),
},
async input => executeAction({ kind: 'press_keys', input }, 'desktop_press_keys'),
)
server.tool(
'desktop_scroll',
{
x: z.number().optional().describe('Optional global logical screen X coordinate to move to before scrolling'),
y: z.number().optional().describe('Optional global logical screen Y coordinate to move to before scrolling'),
deltaX: z.number().optional().describe('Horizontal scroll delta in pixels'),
deltaY: z.number().describe('Vertical scroll delta in pixels'),
captureAfter: z.boolean().optional().describe('Whether to return a fresh screenshot after the action'),
},
async input => executeAction({ kind: 'scroll', input }, 'desktop_scroll'),
)
server.tool(
'desktop_wait',
{
durationMs: z.number().int().min(0).max(30_000).describe('Wait time in milliseconds'),
captureAfter: z.boolean().optional().describe('Whether to return a fresh screenshot after the wait'),
},
async input => executeAction({ kind: 'wait', input }, 'desktop_wait'),
)
server.tool(
'terminal_exec',
{
command: z.string().min(1).describe('Shell command to execute in the local background runner'),
cwd: z.string().optional().describe('Optional working directory override'),
timeoutMs: z.number().int().min(1).max(120_000).optional().describe('Optional timeout override in milliseconds'),
},
async (input: TerminalExecActionInput) => executeAction({ kind: 'terminal_exec', input }, 'terminal_exec'),
)
server.tool(
'terminal_get_state',
{},
async () => {
const terminalState = runtime.session.getTerminalState()
return {
content: [
textContent(`Terminal runner cwd=${terminalState.effectiveCwd}, lastExitCode=${terminalState.lastExitCode ?? 'n/a'}, lastCommand=${terminalState.lastCommandSummary || 'n/a'}.`),
],
structuredContent: {
status: 'ok',
terminalState,
},
}
},
)
server.tool(
'terminal_reset_state',
{
reason: z.string().optional().describe('Optional reset note for the audit log'),
},
async input => executeAction({ kind: 'terminal_reset', input }, 'terminal_reset_state'),
)
server.tool(
'secret_read_env_value',
{
filePath: z.string().min(1).describe('Absolute or explicit env file path to inspect, for example /Users/example-user/airi/.env'),
keys: z.array(z.string().min(1)).min(1).max(16).describe('Candidate env variable names to try in order, e.g. ["AIRI_E2E_DISCORD_TOKEN", "DISCORD_BOT_TOKEN"]'),
allowPlaceholder: z.boolean().optional().describe('Whether to allow obvious placeholder/template values such as replace-with-your-token'),
},
async (input: SecretReadEnvValueActionInput) => executeAction({ kind: 'secret_read_env_value', input }, 'secret_read_env_value'),
)
server.tool(
'clipboard_read_text',
{
maxLength: z.number().int().min(1).max(32_768).optional().describe('Optional maximum number of characters to return from the clipboard'),
trim: z.boolean().optional().describe('Whether to trim leading/trailing whitespace before returning the text (default: true)'),
},
async input => executeAction({ kind: 'clipboard_read_text', input }, 'clipboard_read_text'),
)
server.tool(
'clipboard_write_text',
{
text: z.string().describe('Text to place into the system clipboard'),
},
async input => executeAction({ kind: 'clipboard_write_text', input }, 'clipboard_write_text'),
)
server.tool(
'browser_dom_get_bridge_status',
{},
async () => {
const bridge = runtime.browserDomBridge.getStatus()
return {
content: [
textContent(`Browser DOM bridge ${bridge.connected ? 'connected' : 'disconnected'} on ws://${bridge.host}:${bridge.port}.`),
],
structuredContent: {
status: 'ok',
bridge,
},
}
},
)
server.tool(
'browser_agent_get_status',
{},
async () => {
const launchContext = getBrowserAgentLaunchContext()
return {
content: [
textContent(`Browser agent root ${launchContext.rootExists ? 'ready' : 'missing'} at ${launchContext.cliCwd}; python=${launchContext.pythonCommand}; cdp=${launchContext.cdpUrl}.`),
],
structuredContent: {
status: launchContext.rootExists ? 'ok' : 'missing',
browserAgent: launchContext,
},
}
},
)
server.tool(
'browser_agent_run',
{
instruction: z.string().min(1).describe('Goal-driven browser instruction for the autonomous browser agent.'),
agent: z.enum(['google', 'kimi']).optional().describe('Browser agent backend to use (default: google).'),
cdpUrl: z.string().optional().describe('Optional Chrome CDP endpoint override, e.g. http://localhost:9222'),
maxTurns: z.number().int().min(1).max(80).optional().describe('Maximum browser-agent reasoning turns (default: 30).'),
timeoutMs: z.number().int().min(1_000).max(900_000).optional().describe('End-to-end timeout for the delegated browser task (default: 180000).'),
},
async ({ instruction, agent, cdpUrl, maxTurns, timeoutMs }) => {
const launchContext = getBrowserAgentLaunchContext({ cdpUrl })
if (!launchContext.rootExists) {
return {
isError: true,
content: [
textContent(`Browser agent root is missing: ${launchContext.cliCwd}.`),
],
structuredContent: {
status: 'missing',
browserAgent: launchContext,
},
}
}
try {
const result = await runBrowserAgentTask({
instruction,
agent,
cdpUrl,
maxTurns,
timeoutMs,
})
return {
content: [
textContent(`Browser agent ${result.success ? 'completed' : 'stopped'} on ${result.payload?.url || result.cdpUrl}.`),
],
structuredContent: {
status: result.success ? 'completed' : 'failed',
browserAgent: {
instruction: result.instruction,
agent: result.agent,
cdpUrl: result.cdpUrl,
cliCwd: result.cliCwd,
cliModule: result.cliModule,
pythonCommand: result.pythonCommand,
exitCode: result.exitCode,
timedOut: result.timedOut,
stderrLines: result.stderrLines,
},
payload: result.payload,
},
}
}
catch (error) {
return {
isError: true,
content: [
textContent(`Browser agent failed: ${error instanceof Error ? error.message : String(error)}`),
],
structuredContent: {
status: 'error',
browserAgent: launchContext,
error: error instanceof Error ? error.message : String(error),
},
}
}
},
)
server.tool(
'browser_dom_get_active_tab',
{},
async () => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const activeTab = await runtime.browserDomBridge.getActiveTab()
return {
content: [
textContent(`Active browser tab: ${String(activeTab?.title || activeTab?.url || 'unknown')}.`),
],
structuredContent: {
status: 'ok',
activeTab,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_read_page',
{
includeText: z.boolean().optional().describe('Whether to include truncated body text for each frame'),
maxElements: z.number().int().min(1).max(500).optional().describe('Maximum interactive elements per frame to collect'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ includeText, maxElements, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const frames = await runtime.browserDomBridge.readAllFramesDom({
includeText,
maxElements,
tabId,
frameIds,
})
const interactiveElementCount = frames.reduce((count, frame) => {
const payload = unwrapBrowserDomResult(frame.result)
const record = toBrowserDomRecord(payload)
const elements = Array.isArray(record?.interactiveElements) ? record.interactiveElements : []
return count + elements.length
}, 0)
return {
content: [
textContent(`Read DOM from ${frames.length} frame(s); collected ${interactiveElementCount} interactive element(s).`),
],
structuredContent: {
status: 'ok',
frames,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_find_elements',
{
selector: z.string().min(1).describe('CSS selector to query in the active tab frames'),
maxResults: z.number().int().min(1).max(50).optional().describe('Maximum matched elements to include per frame'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, maxResults, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const results = await runtime.browserDomBridge.findElements({
selector,
maxResults,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`find_elements for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_click',
{
selector: z.string().min(1).describe('CSS selector to click via the browser extension bridge'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['getClickTarget', 'clickAt']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const result = await runtime.browserDomBridge.clickSelector({
selector,
tabId,
frameIds,
})
// NOTICE: clickSelector resolves even when the clickAt step misses
// (e.g. reflow between target lookup and click dispatch). Inspect
// per-frame results before reporting success.
const clickFrames = result?.clickResults
const anyClickSucceeded = Array.isArray(clickFrames) && clickFrames.some(
fr => (fr.result as Record<string, unknown>)?.success === true,
)
if (!anyClickSucceeded) {
return {
isError: true,
content: [
textContent(`browser_dom_click: clicked at (${result.targetPoint.x}, ${result.targetPoint.y}) in frame ${result.targetFrameId} but no frame reported a successful DOM click for "${selector}".`),
],
structuredContent: {
status: 'click_miss',
selector,
...result,
bridge: runtime.browserDomBridge.getStatus(),
},
}
}
return {
content: [
textContent(`Clicked selector "${selector}" in frame ${result.targetFrameId} at (${result.targetPoint.x}, ${result.targetPoint.y}).`),
],
structuredContent: {
status: 'ok',
selector,
...result,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_read_input_value',
{
selector: z.string().min(1).describe('CSS selector for the input/select/textarea element'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['readInputValue']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const results = await runtime.browserDomBridge.readInputValue({
selector,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`read_input_value for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_set_input_value',
{
selector: z.string().min(1).describe('CSS selector for the input/select/textarea element'),
value: z.string().describe('Value to assign to the matched element'),
simulateKeystrokes: z.boolean().optional().describe('Whether to emit a per-character key/input chain'),
blur: z.boolean().optional().describe('Whether to blur the element after setting the value'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, value, simulateKeystrokes, blur, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['setInputValue']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const results = await runtime.browserDomBridge.setInputValue({
selector,
value,
simulateKeystrokes,
blur,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`set_input_value for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
valueLength: value.length,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_check_checkbox',
{
selector: z.string().min(1).describe('CSS selector for the checkbox or radio-like element'),
checked: z.boolean().optional().describe('Target checked state; omit to toggle'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, checked, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['checkCheckbox']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const results = await runtime.browserDomBridge.checkCheckbox({
selector,
checked,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`check_checkbox for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
checked,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_select_option',
{
selector: z.string().min(1).describe('CSS selector for the <select> element'),
value: z.string().min(1).describe('Option value or visible text to select'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, value, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['selectOption']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const results = await runtime.browserDomBridge.selectOption({
selector,
value,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`select_option for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
value,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_wait_for_element',
{
selector: z.string().min(1).describe('CSS selector to wait for'),
timeoutMs: z.number().int().min(1).max(30_000).optional().describe('How long to wait before timing out'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, timeoutMs, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['waitForElement']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const results = await runtime.browserDomBridge.waitForElement({
selector,
timeoutMs,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`wait_for_element for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
timeoutMs: timeoutMs ?? runtime.config.browserDomBridge.requestTimeoutMs,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_get_element_attributes',
{
selector: z.string().min(1).describe('CSS selector for the target element'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const results = await runtime.browserDomBridge.getElementAttributes({
selector,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`get_element_attributes for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_get_computed_styles',
{
selector: z.string().min(1).describe('CSS selector for the target element'),
properties: z.array(z.string()).min(1).max(32).optional().describe('Optional subset of CSS properties to return'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, properties, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['getComputedStyles']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
const results = await runtime.browserDomBridge.getComputedStyles({
selector,
properties,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`get_computed_styles for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'browser_dom_trigger_event',
{
selector: z.string().min(1).describe('CSS selector for the target element'),
eventName: z.string().min(1).describe('Event name to dispatch, e.g. click, input, change'),
eventType: z.enum(['Event', 'MouseEvent', 'KeyboardEvent', 'InputEvent', 'FocusEvent']).optional().describe('DOM event constructor to use'),
optsJson: z.string().optional().describe('Optional JSON object merged into the dispatched event init'),
tabId: optionalTabIdSchema,
frameIds: optionalFrameIdsSchema,
},
async ({ selector, eventName, eventType, optsJson, tabId, frameIds }) => {
if (!runtime.browserDomBridge.getStatus().connected)
return buildBrowserDomUnavailableResponse(runtime)
const requiredActions = ['triggerEvent']
if (!isBrowserDomActionSupported(runtime.browserDomBridge, ...requiredActions))
return buildBrowserDomUnavailableResponse(runtime, getUnsupportedBrowserDomActions(runtime.browserDomBridge, ...requiredActions))
let opts: Record<string, unknown> | undefined
if (optsJson?.trim()) {
let parsed: unknown
try {
parsed = JSON.parse(optsJson) as unknown
}
catch (error) {
return {
isError: true,
content: [
textContent(`browser_dom_trigger_event expected optsJson to be valid JSON: ${error instanceof Error ? error.message : String(error)}`),
],
structuredContent: {
status: 'invalid_params',
field: 'optsJson',
},
}
}
const record = toBrowserDomRecord(parsed)
if (!record) {
return {
isError: true,
content: [
textContent('browser_dom_trigger_event expected optsJson to parse into a JSON object.'),
],
}
}
opts = record
}
const results = await runtime.browserDomBridge.triggerEvent({
selector,
eventName,
eventType,
opts,
tabId,
frameIds,
})
return {
content: [
textContent(summarizeBrowserDomFrameResults(`trigger_event ${eventName} for "${selector}"`, results)),
],
structuredContent: {
status: 'ok',
selector,
eventName,
eventType,
results,
bridge: runtime.browserDomBridge.getStatus(),
},
}
},
)
server.tool(
'desktop_list_pending_actions',
{},
async () => {
const pendingActions = runtime.session.listPendingActions()
return {
content: [
textContent(`Pending actions: ${pendingActions.length}`),
],
structuredContent: {
status: 'ok',
pendingActions,
},
}
},
)
server.tool(
'desktop_approve_pending_action',
{
id: z.string().min(1).describe('Pending action id returned by another desktop tool'),
},
async ({ id }) => {
const pending = runtime.session.getPendingAction(id)
if (!pending) {
return {
isError: true,
content: [
textContent(`Pending action not found: ${id}`),
],
}
}
runtime.session.removePendingAction(id)
runtime.stateManager.recordApprovalOutcome(false)
runtime.stateManager.setPendingApprovalCount(runtime.session.listPendingActions().length)
await runtime.session.record({
event: 'approved',
toolName: 'desktop_approve_pending_action',
action: pending.action,
context: pending.context,
policy: pending.policy,
result: {
pendingActionId: id,
},
})
if (pending.action.kind === 'pty_create') {
const result = await executeApprovedPtyCreate(runtime, pending.action.input)
await runtime.session.record({
event: result.isError === true ? 'failed' : 'executed',
toolName: pending.toolName,
action: pending.action,
context: pending.context,
policy: pending.policy,
result: {
pendingActionId: id,
...(typeof result.structuredContent === 'object' && result.structuredContent !== null
? result.structuredContent as Record<string, unknown>
: {}),
},
})
return result
}
if (pending.action.kind === 'desktop_ensure_chrome') {
const result = await executeChromeEnsure(
runtime,
pending.action.input,
pending.policy.estimatedOperationUnits,
)
await runtime.session.record({
event: result.isError === true ? 'failed' : 'executed',
toolName: pending.toolName,
action: pending.action,
context: pending.context,
policy: pending.policy,
result: {
pendingActionId: id,
...(typeof result.structuredContent === 'object' && result.structuredContent !== null
? result.structuredContent as Record<string, unknown>
: {}),
},
})
return result
}
return await executeAction(pending.action, pending.toolName, {
skipApprovalQueue: true,
})
},
)
server.tool(
'desktop_reject_pending_action',
{
id: z.string().min(1).describe('Pending action id returned by another desktop tool'),
reason: z.string().optional().describe('Optional rejection note for the audit log'),
},
async ({ id, reason }) => {
const pending = runtime.session.getPendingAction(id)
if (!pending) {
return {
isError: true,
content: [
textContent(`Pending action not found: ${id}`),
],
}
}
runtime.session.removePendingAction(id)
runtime.stateManager.recordApprovalOutcome(true, reason)
runtime.stateManager.setPendingApprovalCount(runtime.session.listPendingActions().length)
await runtime.session.record({
event: 'rejected',
toolName: 'desktop_reject_pending_action',
action: pending.action,
context: pending.context,
policy: pending.policy,
result: {
pendingActionId: id,
reason,
},
})
return {
content: [
textContent(`Pending action rejected: ${id}${reason ? ` (${reason})` : ''}. The strategy layer will suggest an alternative approach.`),
],
structuredContent: {
status: 'rejected',
pendingActionId: id,
reason,
},
}
},
)
server.tool(
'desktop_get_session_trace',
{
limit: z.number().int().min(1).max(200).optional().describe('How many recent trace entries to return'),
},
async ({ limit }) => {
const trace = runtime.session.getRecentTrace(limit)
return {
content: [
textContent(`Trace entries returned: ${trace.length}`),
],
structuredContent: {
status: 'ok',
trace,
},
}
},
)
// ---------------------------------------------------------------------------
// Run-level state tool
// ---------------------------------------------------------------------------
server.tool(
'desktop_get_state',
{},
async () => {
await refreshWorkflowRunState()
const state = runtime.stateManager.getState()
const summary = summarizeRunState(state)
return {
content: [textContent(summary)],
structuredContent: {
status: 'ok',
runState: state,
},
}
},
)
// ---------------------------------------------------------------------------
// Workflow tools — unified outward formatter
// ---------------------------------------------------------------------------
function formatWorkflowResult(
workflowId: string,
result: import('../workflows').WorkflowExecutionResult,
) {
return {
content: [textContent(result.summary)],
structuredContent: formatWorkflowStructuredContent({
workflowId,
result,
runState: runtime.stateManager.getState(),
}),
}
}
server.tool(
'workflow_open_workspace',
{
projectPath: z.string().min(1).describe('Absolute path to the project directory'),
ideApp: z.string().optional().describe('IDE application to open the workspace with (default: Cursor)'),
fileManagerApp: z.string().optional().describe('File manager to reveal the workspace in (default: Finder)'),
autoApprove: z.boolean().optional().describe('Skip per-step approval for workflow actions (default: true)'),
},
async ({ projectPath, ideApp, fileManagerApp, autoApprove }) => {
const workflow = createDevOpenWorkspaceWorkflow({ projectPath, ideApp, fileManagerApp })
const result = await executeWorkflow({
workflow,
executeAction,
executePrepTool,
acquirePty,
stateManager: runtime.stateManager,
refreshState: refreshWorkflowRunState,
overrides: { projectPath },
autoApproveSteps: autoApprove ?? true,
})
suspendedWorkflow = result.suspension
return formatWorkflowResult(workflow.id, result)
},
)
server.tool(
'workflow_validate_workspace',
{
projectPath: z.string().min(1).describe('Absolute path to the project directory'),
ideApp: z.string().optional().describe('IDE application to open the workspace with (default: Cursor)'),
fileManagerApp: z.string().optional().describe('File manager to reveal the workspace in (default: Finder)'),
changesCommand: z.string().optional().describe('Command to inspect local changes (default: git diff --stat)'),
checkCommand: z.string().optional().describe('Validation command to run from the workspace root (default: pnpm typecheck)'),
autoApprove: z.boolean().optional().describe('Skip per-step approval for workflow actions (default: true)'),
},
async ({ projectPath, ideApp, fileManagerApp, changesCommand, checkCommand, autoApprove }) => {
const workflow = createDevValidateWorkspaceWorkflow({
projectPath,
ideApp,
fileManagerApp,
changesCommand,
checkCommand,
})
const result = await executeWorkflow({
workflow,
executeAction,
executePrepTool,
acquirePty,
stateManager: runtime.stateManager,
refreshState: refreshWorkflowRunState,
overrides: { projectPath },
autoApproveSteps: autoApprove ?? true,
})
suspendedWorkflow = result.suspension
return formatWorkflowResult(workflow.id, result)
},
)
server.tool(
'workflow_run_tests',
{
projectPath: z.string().min(1).describe('Absolute path to the project directory'),
testCommand: z.string().optional().describe('Shell command to run tests (default: pnpm test:run)'),
autoApprove: z.boolean().optional().describe('Skip per-step approval for workflow actions (default: true)'),
},
async ({ projectPath, testCommand, autoApprove }) => {
const workflow = createDevRunTestsWorkflow({ projectPath, testCommand })
const result = await executeWorkflow({
workflow,
executeAction,
executePrepTool,
acquirePty,
stateManager: runtime.stateManager,
refreshState: refreshWorkflowRunState,
overrides: { projectPath },
autoApproveSteps: autoApprove ?? true,
})
// Store suspension for resume capability.
suspendedWorkflow = result.suspension
return formatWorkflowResult(workflow.id, result)
},
)
server.tool(
'workflow_inspect_failure',
{
ideApp: z.string().optional().describe('IDE application to focus (default: Cursor)'),
diagnosticCommand: z.string().optional().describe('Optional command to re-run for fresh error output'),
autoApprove: z.boolean().optional().describe('Skip per-step approval for workflow actions (default: true)'),
},
async ({ ideApp, diagnosticCommand, autoApprove }) => {
const workflow = createDevInspectFailureWorkflow({ ideApp, diagnosticCommand })
const result = await executeWorkflow({
workflow,
executeAction,
executePrepTool,
acquirePty,
stateManager: runtime.stateManager,
refreshState: refreshWorkflowRunState,
autoApproveSteps: autoApprove ?? true,
})
suspendedWorkflow = result.suspension
return formatWorkflowResult(workflow.id, result)
},
)
server.tool(
'workflow_browse_and_act',
{
app: z.string().optional().describe('Application to open (default: Google Chrome)'),
goal: z.string().optional().describe('Short description of what to accomplish'),
url: z.string().optional().describe('Optional URL to navigate to in the browser'),
autoApprove: z.boolean().optional().describe('Skip per-step approval for workflow actions (default: true)'),
},
async ({ app, goal, url, autoApprove }) => {
const workflow = createAppBrowseAndActWorkflow({ app, goal, url })
const result = await executeWorkflow({
workflow,
executeAction,
executePrepTool,
acquirePty,
stateManager: runtime.stateManager,
refreshState: refreshWorkflowRunState,
autoApproveSteps: autoApprove ?? true,
})
suspendedWorkflow = result.suspension
return formatWorkflowResult(workflow.id, result)
},
)
server.tool(
'workflow_resume',
{
approved: z.boolean().optional().describe('Whether the pending step was approved (default: true)'),
autoApprove: z.boolean().optional().describe('Skip per-step approval for remaining steps (default: true)'),
},
async ({ approved, autoApprove }) => {
if (!suspendedWorkflow) {
return {
isError: true,
content: [textContent('No suspended workflow to resume. Start a workflow first.')],
structuredContent: { status: 'error', reason: 'no_suspended_workflow' },
}
}
const suspension = suspendedWorkflow
suspendedWorkflow = undefined
const result = await resumeWorkflow({
suspension,
executeAction,
executePrepTool,
acquirePty,
stateManager: runtime.stateManager,
refreshState: refreshWorkflowRunState,
approved: approved ?? true,
autoApproveSteps: autoApprove ?? true,
})
// Store new suspension if workflow pauses again.
suspendedWorkflow = result.suspension
return formatWorkflowResult(suspension.workflow.id, result)
},
)
}