server-multiuser.js
Rama main · réplica de lectura del repositorio original.
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
if (process.env.YOURHAND_ENV_FILE) require('dotenv').config({ path: process.env.YOURHAND_ENV_FILE, override: false });
const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');
const https = require('https');
const crypto = require('crypto');
const { AsyncLocalStorage } = require('async_hooks');
const { YourHandStore } = require('./src/multiuser/store');
const { DurableTaskLedger } = require('./src/multiuser/task-ledger');
const { DeviceTaskLocks } = require('./src/multiuser/device-task-locks');
const { OperationJournal } = require('./src/multiuser/operation-journal');
const { CallTelemetry,codeOf,methodOf,routeOf,performance } = require('./src/multiuser/call-telemetry');
const { UsageMeter } = require('./src/multiuser/usage-meter');
const { ExecutionEngine } = require('./src/execution/engine');
const { BrowserExecution } = require('./src/execution/browser');
const { chooseRoute } = require('./src/execution/capability-router');
const { YourHandOAuth } = require('./src/multiuser/oauth');
const { createWebApp } = require('./src/multiuser/web');
const { WebSocketServer } = require('ws');
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
const { createMcpExpressApp } = require('@modelcontextprotocol/sdk/server/express.js');
const z = require('zod/v4');
const VERSION = '0.9.0-alpha';
const PRODUCT_NAME = 'YourHand';
const AGENT_PORT = Number(process.env.YOURHAND_AGENT_PORT || 8791);
const MCP_PORT = Number(process.env.YOURHAND_MCP_PORT || 8792);
const WEB_PORT = Number(process.env.YOURHAND_WEB_PORT || 8790);
const DB_FILE = process.env.YOURHAND_DB_FILE || path.join(__dirname, 'runtime', 'yourhand-community.db');
const BASE_URL = process.env.YOURHAND_BASE_URL || `http://127.0.0.1:${WEB_PORT}`;
const MCP_PUBLIC_URL = process.env.YOURHAND_MCP_PUBLIC_URL || `${BASE_URL.replace(/\/$/,'')}/mcp`;
const WEB_DIR = path.join(__dirname, 'web');
const store = new YourHandStore(DB_FILE);
const taskLedger = new DurableTaskLedger(store);
const operationJournal = new OperationJournal({
db:store.db,
getDeviceForUser:(userId,deviceId)=>{
const access=store.getDeviceForUser(userId,deviceId);
return !!access && access.access_role!=='viewer';
}
});
const EXECUTION_OWNER_EPOCH=crypto.randomUUID();
const oauth = new YourHandOAuth(store,{issuer:BASE_URL,resource:MCP_PUBLIC_URL});
const userContext = new AsyncLocalStorage();
const traceContext = new AsyncLocalStorage();
const telemetry = new CallTelemetry({db:store.db,retentionDays:process.env.YOURHAND_TELEMETRY_RETENTION_DAYS||30});
const usageMeter = new UsageMeter({db:store.db});
telemetry.prune();
const telemetryPruneTimer=setInterval(()=>{try{telemetry.prune()}catch{}},6*60*60*1000);
telemetryPruneTimer.unref();
const devices = new Map();
const pending = new Map();
const taskLocks = new DeviceTaskLocks();
function jsonText(value) {
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
}function normalizeResult(value) {
if (value && Array.isArray(value.content)) return value;
return jsonText(value);
}
const LEGACY_OWNER_USER_ID = String(process.env.YOURHAND_LEGACY_OWNER_USER_ID || '').trim();
const LEGACY_OWNER_DEVICE = String(process.env.YOURHAND_LEGACY_OWNER_DEVICE || '').trim();
const LEGACY_PUBLIC_KEY_DIR = process.env.YOURHAND_LEGACY_PUBLIC_KEY_DIR || './runtime/legacy-public-keys';
const LEGACY_MCP_AUTH_MESSAGE = 'yourhand-mcp-owner-v1';
let LEGACY_WEB_MCP_PATH = null;
try {
const candidate = String(process.env.YOURHAND_LEGACY_WEB_MCP_PATH || '').trim();
if (/^\/yh-web-[A-Za-z0-9_-]{20,}$/.test(candidate)) LEGACY_WEB_MCP_PATH = candidate;
} catch {}
function legacyOwnerFromBearer(token) {
if (!LEGACY_OWNER_USER_ID || !token) return null;
if (!/^[A-Za-z0-9._-]{1,128}$/.test(LEGACY_OWNER_DEVICE)) return null;
let pubKey;
try { pubKey = fs.readFileSync(path.join(LEGACY_PUBLIC_KEY_DIR, LEGACY_OWNER_DEVICE + '.pem'), 'utf8'); }
catch { return null; }
let ok = false;
try { ok = crypto.verify(null, Buffer.from(LEGACY_MCP_AUTH_MESSAGE, 'utf8'), pubKey, Buffer.from(token, 'base64url')); }
catch { return null; }
if (!ok) return null;
return store.db.prepare('SELECT id,email,name,picture FROM users WHERE id=?').get(LEGACY_OWNER_USER_ID) || null;
}
function requireMcpAuth(req, res, next) {
const header = String(req.headers.authorization || '');
const token = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
let user = oauth.userFromAccessToken(token);
const pathOnly = String(req.originalUrl || ((req.baseUrl || '') + (req.path || ''))).split('?')[0];
if (!user && (pathOnly === '/mcp' || pathOnly === '/yourhand-rpc' || (LEGACY_WEB_MCP_PATH && pathOnly === LEGACY_WEB_MCP_PATH))) user = legacyOwnerFromBearer(token);
if (!user) {
res.setHeader('WWW-Authenticate',`Bearer resource_metadata="${BASE_URL.replace(/\/$/,'')}/.well-known/oauth-protected-resource", error="invalid_token", error_description="Connect your YourHand account"`);
return res.status(401).json({ error: 'Unauthorized' });
}
req.yhUser = user;
next();
}
function currentUser() {
const user = userContext.getStore();
if (!user) throw new Error('Unauthorized');
return user;
}
function publicDevice(d) {
return {
id: d.id,
name: d.name,
hostname: d.meta.hostname || null,
platform: d.meta.platform || null,
arch: d.meta.arch || null,
agentVersion: d.meta.agentVersion || null,
dcVersion: d.meta.dcVersion || null,
toolCount: d.tools.length,
connectedAt: d.connectedAt,
lastSeen: d.lastSeen
};
}
function getDevice(ref, mode='control') {
const user = currentUser();
let d = devices.get(ref);
if (!d) d = [...devices.values()].find(x => x.name === ref && store.getDeviceForUser(user.id,x.id));
const access=d&&store.getDeviceForUser(user.id,d.id);
if(!d||!access||d.ws.readyState!==1||!store.getActiveDevice(d.id))throw new Error('Device not found');
if(mode==='control' && access.access_role==='viewer')throw new Error('Operator access required for device commands');
if(user.scope){
const granted=new Set(String(user.scope).split(/\s+/));
if(!granted.has(mode==='control'?'devices.control':'devices.read'))throw new Error('OAuth scope does not authorize device '+mode);
}
return d;
}
// Only explicitly whitelisted observational calls bypass the exclusive mutating-task lock.
const READONLY_DC_TOOLS=new Set(['read_file','read_multiple_files','list_directory','get_file_info','list_processes','list_sessions','get_usage_stats']);
const READONLY_FS_ACTIONS=new Set(['stat','list','read_text','read_binary','search']);
function isReadOnlyRpc(method,params={}) {
if(['ping','system_info','native_screenshot','native_observe','browser_screenshot'].includes(method))return true;
if(method==='fs_call'&&READONLY_FS_ACTIONS.has(params.action))return true;
if(method==='mcp_call'&&READONLY_DC_TOOLS.has(params.tool))return true;
if(method==='proc_call'&&['list','read'].includes(params.action))return true;
if(method==='guarded_action_status')return true;
if(method==='native_call'&&['screenshot','observe','desktop_context','session_state','foreground','list_windows','semantic_snapshot','ui_tree','ui_find'].includes(params.action))return true;
if(method==='browser_call'&&['status','tabs'].includes(params.action))return true;
return false;
}
function rpcDispatch(deviceRef, method, params = {}, timeoutMs = 60000, traceMeta = null) {
const mode=isReadOnlyRpc(method,params)?'read':'control';
const d=getDevice(deviceRef,mode);
// Viewer exposes only device overview/health; raw filesystem/screen contents
// require the owner's explicit Operator grant even when the RPC is read-only.
if(mode==='read' && !['ping','system_info'].includes(method) &&
store.getDeviceForUser(currentUser().id,d.id).access_role==='viewer')
throw new Error('Operator permission required for device content');
const id = crypto.randomUUID();
const caller=currentUser();
let autoLease=null;
if(mode==='control'){
// Explicit interactive task reservations remain sticky by design. A plain
// one-shot command must not reserve the whole shared PC for 15 minutes.
taskLocks.dropIfUnauthorized(d.id,lockUserId=>{
const row=store.getDeviceForUser(lockUserId,d.id);
return !!row&&row.access_role!=='viewer';
});
const existing=taskLocks.info(d.id,caller.id);
if(!existing.ownedByMe){
autoLease='rpc_'+id.replace(/-/g,'');
taskLocks.acquire(d.id,caller.id,autoLease);
}else if(existing.taskId&&existing.taskId.startsWith('rpc_')){
throw Error('Device is executing another command; wait for the active result');
}
}
const releaseFinished=()=>{
if(autoLease){try{taskLocks.release(d.id,caller.id,autoLease)}catch{}}
};
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
// Outcome uncertain: retain temporary reservation until lease expires.
// Never grant another account GUI control while a timed-out side effect
// may still be executing on the Agent.
reject(new Error(`RPC timeout after ${timeoutMs}ms; outcome uncertain`));
}, Math.min(Math.max(timeoutMs, 1000), 180000));
pending.set(id, {
traceMeta,
resolve:value=>{releaseFinished();resolve(value)},
reject:error=>{if(!/disconnect|outcome uncertain/i.test(String(error?.message||error)))releaseFinished();reject(error)},
timer,deviceId:d.id,ws:d.ws,requesterId:caller.id
});
try {
d.ws.send(JSON.stringify({ type: 'request', id, method, params }), err => {
if (err && pending.get(id)?.ws === d.ws) {
clearTimeout(timer); pending.delete(id);releaseFinished();reject(err);
}
});
} catch (err) { clearTimeout(timer); pending.delete(id);releaseFinished();reject(err); }
});
}
function recordInnerSteps(meta,common,trace){
if(!Array.isArray(meta?.steps))return;
for(const step of meta.steps.slice(0,64)){
if(!step||typeof step.methodId!=='string'||!/^AGENT_(?:NATIVE|BROWSER)_[A-Z0-9_]{1,46}$/.test(step.methodId))continue;
telemetry.record({...common,eventType:'agent_rpc',methodId:step.methodId,
routeId:step.routeId,attemptIndex:step.attemptIndex,
methodsTried:meta.steps.length,durationMs:step.durationMs,
outcome:step.outcome==='completed'?'completed':'failed',
errorCode:step.errorCode,errorStage:step.outcome==='completed'?null:'AGENT_INTERNAL'});
}
}
// Every authenticated low-level attempt creates an append-only diagnostic
// event. Raw command text, arguments, URLs, device content and exceptions
// NEVER enter the telemetry DB. A failed pre-dispatch auth/lock check is
// recorded too; repeated attempts have a common trace ID.
async function rpc(deviceRef, method, params = {}, timeoutMs = 60000){
const began=performance.now();
const user=currentUser();
const trace=traceContext.getStore();
const attempt=trace?++trace.attempts:1;
const methodId=methodOf(method,params);
if(trace)trace.methods.add(methodId);
const authorized=store.getDeviceForUser(user.id,deviceRef)||
store.listDevices(user.id).find(x=>x.display_name===deviceRef);
const deviceId=authorized?.id||null;
const traceMeta={agentDurationMs:null,steps:[]};
const common={traceId:trace?.id,userId:user.id,deviceId,eventType:'agent_rpc',
methodId,routeId:routeOf(method,params),attemptIndex:attempt};
try{
const result=await rpcDispatch(deviceRef,method,params,timeoutMs,traceMeta);
recordInnerSteps(traceMeta,common,trace);
const state=['failed','needs_review','timed_out'].includes(result?.status)?result.status:
result?.ok===false?'failed':'completed';
const elapsed=performance.now()-began;
telemetry.record({...common,durationMs:elapsed,
agentDurationMs:traceMeta.agentDurationMs,
networkQueueMs:traceMeta.agentDurationMs===null?null:Math.max(0,elapsed-traceMeta.agentDurationMs),
methodsTried:trace?.methods.size||1,outcome:state,
errorCode:result?.error?.code||result?.error?.message||null,
errorStage:state==='completed'?null:'AGENT_RESULT'});
return result;
}catch(error){
recordInnerSteps(traceMeta,common,trace);
const code=codeOf(error?.message||error?.code);
telemetry.record({...common,durationMs:performance.now()-began,
methodsTried:trace?.methods.size||1,
outcome:code==='TIMEOUT'?'timed_out':
code==='CONNECTION_LOST'?'disconnected':'failed',
errorCode:error?.message||error?.code,errorStage:deviceId?'RPC_OR_AGENT':'AUTH_OR_DEVICE'});
throw error;
}
}
// Execution facade coexists with the original typed tools and uses their existing
// per-account/device authorization, shared task locks, WebSocket response binding.
const execution = new ExecutionEngine({invokeNative:(deviceId,method,args,timeoutMs)=>rpc(deviceId,method,args,timeoutMs)});
const browserExecution = new BrowserExecution({call:(deviceId,action,args)=>rpc(deviceId,'browser_call',{action,args},20000)});
// Persistent idempotency and fail-closed recovery across Core process changes.
// Old in-memory engine caches are insufficient if a click was sent before a crash.
async function durableComputerAction(device,idempotencyKey,kind,payload,perform){
const authorized=getDevice(device,'control'),deviceId=authorized.id,userId=currentUser().id;
const operationId='op_'+crypto.createHash('sha256').update(idempotencyKey).digest('hex').slice(0,64);
const fingerprint={kind,requestHash:crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex')};
const prior=operationJournal.row(userId,deviceId,operationId);
let record;
try{record=operationJournal.submit(userId,deviceId,operationId,fingerprint);}
catch(error){
if(String(error?.message||'').startsWith('Idempotency conflict:'))
return {operationId,status:'failed',effect:'rejected',goal:'not_verified',
error:{code:'INVALID_REQUEST',message:'Idempotency key was reused with different arguments; original operation was not redispatched'}};
throw error;
}
if(prior){
if(record.state==='completed' && record.outcome!==null)return JSON.parse(record.outcome);
return {operationId,status:'needs_review',effect:'unverifiable',goal:'not_verified',
error:{code:'OUTCOME_UNCERTAIN',message:'Previous dispatch state is uncertain; inspect device state before starting a DIFFERENT authorized operation. No automatic replay.'}};
}
operationJournal.dispatch(userId,deviceId,operationId,EXECUTION_OWNER_EPOCH);
try{
// Scope the Agent-local action to the authorized user, device and durable
// server record. No account may reuse another account's local action ID.
const actionId='a_'+crypto.createHash('sha256').update(
JSON.stringify([userId,deviceId,operationId])).digest('hex');
const result=await perform({userId,deviceId,actionId});
if(result?.effect==='unverifiable' || result?.status==='timed_out' || result?.status==='needs_review')
operationJournal.uncertain(userId,deviceId,operationId,EXECUTION_OWNER_EPOCH);
else operationJournal.finish(userId,deviceId,operationId,EXECUTION_OWNER_EPOCH,result);
return result;
}catch(error){
try{operationJournal.uncertain(userId,deviceId,operationId,EXECUTION_OWNER_EPOCH)}catch{}
return {operationId,status:'needs_review',effect:'unverifiable',goal:'not_verified',
error:{code:'OUTCOME_UNCERTAIN',message:'Execution request failed after dispatch reservation. Inspect device before any new command. No automatic replay.'}};
}
}
function dropDevice(deviceId, ws, reason) {
// A replaced/stale socket must never remove the new session or reject its requests.
if (devices.get(deviceId)?.ws === ws) {devices.delete(deviceId);taskLocks.forceReleaseDevice(deviceId);}
for (const [id, p] of pending) {
if (p.deviceId !== deviceId || p.ws !== ws) continue;
clearTimeout(p.timer);
pending.delete(id);
p.reject(new Error(`Device disconnected (${reason})`));
}
}
const agentHttp = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true, name: PRODUCT_NAME, version: VERSION, devices: devices.size }));
return;
}
res.writeHead(404).end();
});
const wss = new WebSocketServer({ noServer: true });
agentHttp.on('upgrade', (req, socket, head) => {
const url = new URL(req.url, 'https://127.0.0.1');
if (!['/agent','/agent-v2'].includes(url.pathname)) return socket.destroy();
const deviceId = String(url.searchParams.get('device_id') || '').trim();
const row = store.getActiveDevice(deviceId);
if (!row?.public_key) return socket.destroy();
wss.handleUpgrade(req, socket, head, ws => wss.emit('connection', ws, row));
});
wss.on('connection', (ws, row) => {
let authenticated = false;
let d = null;
const authTimer = setTimeout(() => { if (!authenticated) ws.close(1008, 'Authentication timeout'); }, 10000);
ws._yourHandNonce = crypto.randomBytes(32).toString('base64url');
ws.send(JSON.stringify({ type: 'challenge', nonce: ws._yourHandNonce }));
ws.on('message', raw => {
let msg;
try { msg = JSON.parse(raw.toString()); } catch { return; }
if (!authenticated) {
if (msg.type !== 'auth' || !msg.signature) return ws.close(1008, 'Authentication required');
let ok = false;
try { ok = crypto.verify(null, Buffer.from(ws._yourHandNonce, 'utf8'), row.public_key, Buffer.from(msg.signature, 'base64')); } catch {}
if (!ok) return ws.close(1008, 'Authentication failed');
authenticated = true;
clearTimeout(authTimer);
const old = devices.get(row.id);
if (old?.ws?.readyState === 1) old.ws.close(1012, 'Replaced by new session');
d = { id: row.id, userId: row.user_id, name: row.display_name, ws, meta: {}, tools: [], connectedAt: new Date().toISOString(), lastSeen: new Date().toISOString() };
devices.set(row.id, d);
store.touchDevice(row.id, true);
telemetry.record({traceId:crypto.randomUUID(),userId:row.user_id,deviceId:row.id,
eventType:'agent_socket',methodId:'AGENT_AUTHENTICATED',routeId:'agent.websocket',
attemptIndex:1,methodsTried:1,durationMs:0,outcome:'completed'});
ws.send(JSON.stringify({ type: 'auth_ok', deviceId: row.id }));
console.log(`[YourHand] ${row.display_name} authenticated`);
return;
}
d.lastSeen = new Date().toISOString();
store.touchDevice(d.id, false);
if (msg.type === 'hello') {
d.meta = msg.meta || {};
d.tools = Array.isArray(msg.tools) ? msg.tools : [];
console.log(`[YourHand] ${d.name} online (${d.tools.length} tools)`);
return;
}
if (msg.type === 'response' && pending.has(msg.id)) {
const p = pending.get(msg.id);
if (p.ws !== ws || p.deviceId !== d.id) return; // reject stale or forged cross-session replies
clearTimeout(p.timer);
pending.delete(msg.id);
if(p.traceMeta && Number.isFinite(msg.agentElapsedMs) && msg.agentElapsedMs>=0 && msg.agentElapsedMs<600000)
p.traceMeta.agentDurationMs=Math.round(msg.agentElapsedMs);
if(p.traceMeta&&Array.isArray(msg.agentMethodCalls))
p.traceMeta.steps=msg.agentMethodCalls.filter(x=>x&&typeof x==='object').slice(0,64);
// If the owner revoked this teammate while a request was in flight, never
// disclose its result to the now-unauthorized account. Device actions already
// started cannot be undone; caller must inspect their outcome separately.
if(!store.getDeviceForUser(p.requesterId,d.id))p.reject(new Error('Device access revoked during request; outcome requires review'));
else msg.ok ? p.resolve(msg.result) : p.reject(new Error(msg.error || 'Agent error'));
}
});
ws.on('close', (code, reason) => {
clearTimeout(authTimer);
if (d) {
// Log only non-secret aggregate connection health. Do not expose device
// name, credentials, RPC parameters, screen content, or raw close reason.
const attached=devices.get(d.id)?.ws===ws;
const inflight=[...pending.values()].filter(p=>p.deviceId===d.id&&p.ws===ws).length;
const ageMs=Math.max(0,Date.now()-Date.parse(d.connectedAt||new Date().toISOString()));
console.log('[YourHand] agent_socket_closed',
JSON.stringify({at:new Date().toISOString(),deviceHash:crypto.createHash('sha256').update(d.id).digest('hex').slice(0,12),
code,authenticated,attached,ageMs,inflight}));
telemetry.record({traceId:crypto.randomUUID(),userId:d.userId,deviceId:d.id,
eventType:'agent_socket',methodId:'AGENT_WS_DISCONNECTED',routeId:'agent.websocket',
attemptIndex:1,methodsTried:1,durationMs:Math.max(0,Date.now()-Date.parse(d.connectedAt)),
outcome:'disconnected',errorCode:'CONNECTION_LOST',errorStage:'WEBSOCKET_CLOSE'});
dropDevice(d.id,ws,String(code));
}
});
ws.on('error', err => console.error(`[YourHand] websocket error:`, err.message));
});
function withDevice(schema = {}) {
return { device: z.string().describe('Connected YourHand device name'), ...schema };
}
async function dcCall(device, tool, args, timeoutMs = 120000) {
return normalizeResult(await rpc(device, 'mcp_call', { tool, args }, timeoutMs));
}
function makeMcpServer() {
const mcp = new McpServer({ name: 'yourhand', version: VERSION });
// One trace for each MCP tool invocation (including authorization failures,
// validation errors, cache hits and calls that never reach a device).
const register=mcp.registerTool.bind(mcp);
mcp.registerTool=(name,config,callback)=>{
return register(name,config,async (...args)=>{
const started=performance.now(),usageStartedMs=Date.now(),trace={id:crypto.randomUUID(),attempts:0,methods:new Set()};
const user=currentUser();
const rawDevice=args[0]?.device||args[0]?.device_id||null;
const linked=rawDevice&&(store.getDeviceForUser(user.id,rawDevice)||
store.listDevices(user.id).find(x=>x.display_name===rawDevice));
const common={traceId:trace.id,userId:user.id,deviceId:linked?.id||null,
eventType:'mcp_tool',methodId:'TOOL_'+name.toUpperCase().replace(/[^A-Z0-9_]/g,'_').slice(0,65),
attemptIndex:0};
return traceContext.run(trace,async()=>{
try{
const result=await callback(...args);
// Tool-returned JSON can contain screenshots, files or commands:
// inspect ONLY short metadata when the first result is plain text.
let outcome=result?.isError?'failed':'completed',code=null,route=null;
const first=result?.content?.[0];
if(first?.type==='text'&&typeof first.text==='string'&&first.text.length<=2200&&first.text.trim().startsWith('{')){
try{
const metadata=JSON.parse(first.text);
if(['failed','needs_review','timed_out'].includes(metadata.status))outcome=metadata.status;
if(metadata.ok===false)outcome='failed';
code=metadata.error?.code||metadata.code||null;
route=typeof metadata.routeUsed==='string'?metadata.routeUsed:null;
}catch{}
}
const elapsed=performance.now()-started;
telemetry.record({...common,durationMs:elapsed,
methodsTried:trace.methods.size,outcome,errorCode:code,routeId:route,
errorStage:outcome==='completed'?null:'TOOL_RESULT'});
usageMeter.record({traceId:trace.id,userId:user.id,deviceId:linked?.id,
tool:name,attempts:trace.attempts,methods:trace.methods,
startedMs:usageStartedMs,durationMs:elapsed,success:outcome==='completed'});
return result;
}catch(error){
const elapsed=performance.now()-started;
telemetry.record({...common,durationMs:elapsed,
methodsTried:trace.methods.size,outcome:'failed',
errorCode:error?.message||error?.code,errorStage:'MCP_DISPATCH'});
usageMeter.record({traceId:trace.id,userId:user.id,deviceId:linked?.id,
tool:name,attempts:trace.attempts,methods:trace.methods,
startedMs:usageStartedMs,durationMs:elapsed,success:false});
throw error;
}
});
});
};
mcp.registerTool('list_devices', {
description: 'List YourHand devices that are currently connected.',
inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async () => {
const user = currentUser();
return jsonText([...devices.values()].filter(d => store.getDeviceForUser(user.id,d.id)).map(d=>({...publicDevice(d),accessRole:store.getDeviceForUser(user.id,d.id).access_role})));
});
mcp.registerTool('checkpoint_save', {
description: 'Persist a concise, confirmed task progress checkpoint. Use before and after lengthy work; do not replay an uncertain action after disconnect.',
inputSchema: {
device_id:z.string(), task_id:z.string(), expected_revision:z.number().int().nonnegative(),
status:z.enum(['running','paused','completed','needs_review']),
last_confirmed_step:z.string().max(4000), next_safe_step:z.string().max(4000),
file_path:z.string().max(2048).optional(), notes:z.string().max(4000).optional()
}, annotations:{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
}, async ({device_id,task_id,expected_revision,status,last_confirmed_step,next_safe_step,file_path,notes}) =>
jsonText((()=>{
const linked=store.getDeviceForUser(currentUser().id,device_id);
if(!linked||linked.access_role==='viewer')throw new Error('Device operator permission required');
return taskLedger.save(currentUser().id,task_id,device_id,expected_revision,status,
{lastConfirmedStep:last_confirmed_step,nextSafeStep:next_safe_step,filePath:file_path||'',notes:notes||''});
})()));
mcp.registerTool('checkpoint_get', {
description:'Read the latest persisted checkpoint for a task before attempting to resume it. Never assume an unconfirmed step finished.',
inputSchema:{task_id:z.string()},annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({task_id})=>jsonText(taskLedger.get(currentUser().id,task_id)));
mcp.registerTool('checkpoint_list', {
description:'List persisted task checkpoints for the signed-in account.',
inputSchema:{},annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ()=>jsonText(taskLedger.list(currentUser().id)));
mcp.registerTool('device_lock_status',{
description:'See whether a shared device is currently reserved for a task by any account.',
inputSchema:withDevice(),annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
},async ({device})=>{
const d=getDevice(device,'read');
return jsonText(taskLocks.info(d.id,currentUser().id));
});
mcp.registerTool('acquire_device_lock',{
description:'Reserve the whole interactive device for your own task. Other accounts cannot issue mutating commands until released or lease expiration. No concurrent GUI edits.',
inputSchema:withDevice({task_id:z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,95}$/)}),
annotations:{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
},async ({device,task_id})=>{
const d=getDevice(device,'control');
return jsonText(taskLocks.acquire(d.id,currentUser().id,task_id));
});
mcp.registerTool('release_device_lock',{
description:'Release your own task reservation only after the task has finished or safely paused.',
inputSchema:withDevice({task_id:z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,95}$/)}),
annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
},async ({device,task_id})=>{
const d=getDevice(device,'control');
return jsonText({released:taskLocks.release(d.id,currentUser().id,task_id)});
});
mcp.registerTool('ping_device', {
description: 'Measure connectivity to a YourHand device.',
inputSchema: withDevice(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => {
const t0 = Date.now();
const r = await rpc(device, 'ping', {}, 10000);
return jsonText({ ...r, latencyMs: Date.now() - t0 });
});
mcp.registerTool('get_system_info', {
description: 'Get OS, CPU, memory, uptime and runtime information for a device.',
inputSchema: withDevice(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => jsonText(await rpc(device, 'system_info', {}, 10000)));
mcp.registerTool('list_device_tools', {
description: 'List all local Desktop Commander tools exposed by a device.',
inputSchema: withDevice(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => jsonText(getDevice(device,'read').tools));
mcp.registerTool('call_device_tool', {
description: 'Call any Desktop Commander MCP tool on a connected device. Use list_device_tools first for unfamiliar tools.',
inputSchema: withDevice({
tool: z.string(),
arguments: z.record(z.string(), z.any()).optional()
}), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, tool, arguments: args }) => dcCall(device, tool, args || {})); mcp.registerTool('start_process', {
description: 'Start a terminal command or interactive process on a device.',
inputSchema: withDevice({
command: z.string(), timeout_ms: z.number().int().optional(),
shell: z.string().optional(), verbose_timing: z.boolean().optional()
}), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => dcCall(device, 'start_process', args));
mcp.registerTool('interact_with_process', {
description: 'Send input to a running process and receive its response.',
inputSchema: withDevice({
pid: z.number().int(), input: z.string(), timeout_ms: z.number().int().optional(),
wait_for_prompt: z.boolean().optional(), verbose_timing: z.boolean().optional()
}), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => dcCall(device, 'interact_with_process', args));
mcp.registerTool('read_process_output', {
description: 'Read buffered or new output from a running process.',
inputSchema: withDevice({
pid: z.number().int(), offset: z.number().int().optional(), length: z.number().int().optional(),
timeout_ms: z.number().int().optional(), verbose_timing: z.boolean().optional()
}), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => dcCall(device, 'read_process_output', args)); mcp.registerTool('list_directory', {
description: 'List files and folders on a device.',
inputSchema: withDevice({ path: z.string(), depth: z.number().int().min(1).max(10).optional() }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => dcCall(device, 'list_directory', args));
mcp.registerTool('read_file', {
description: 'Read a file using Desktop Commander format-aware readers.',
inputSchema: withDevice({
path: z.string(), isUrl: z.boolean().optional(), offset: z.number().int().optional(),
length: z.number().int().optional(), sheet: z.string().optional(), range: z.string().optional(),
options: z.record(z.string(), z.any()).optional()
}), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => dcCall(device, 'read_file', args));
mcp.registerTool('read_multiple_files', {
description: 'Read several files from a device in one call.',
inputSchema: withDevice({ paths: z.array(z.string()).min(1).max(50) }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => dcCall(device, 'read_multiple_files', args));
mcp.registerTool('write_file', {
description: 'Create, rewrite or append a file on a device.',
inputSchema: withDevice({ path: z.string(), content: z.string(), mode: z.enum(['rewrite','append']).optional() }),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({ device, ...args }) => dcCall(device, 'write_file', args)); mcp.registerTool('get_config', {
description: 'Read Desktop Commander configuration from a device.',
inputSchema: withDevice(),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => dcCall(device, 'get_config', {}));
const nativeArgsSchema = z.record(z.string(), z.any()).optional();
mcp.registerTool('session_state', {
description: 'Report whether the interactive Windows session is locked/unlocked and whether native input is currently available.',
inputSchema: withDevice(),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => jsonText(await rpc(device, 'native_call', { action:'session_state', args:{} }, 10000)));
mcp.registerTool('native_call', {
description: 'Call a fast YourHand native desktop action on a connected device. Actions include windows, UI Automation, mouse, keyboard, clipboard, screenshot, launch and waits.',
inputSchema: withDevice({
action: z.enum(['ping','list_windows','foreground','focus_window','close_window','mouse','scroll','hotkey','type_text','clipboard_get','clipboard_set','screenshot','ui_tree','ui_find','ui_invoke','ui_set_value','semantic_snapshot','semantic_batch','wait_window','launch','sleep','session_state','desktop_context']),
arguments: nativeArgsSchema,
timeout_ms: z.number().int().min(500).max(180000).optional()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, action, arguments: args, timeout_ms }) =>
jsonText(await rpc(device, 'native_call', { action, args: args || {}, timeoutMs: timeout_ms || 15000 }, (timeout_ms || 15000) + 3000)));
mcp.registerTool('native_batch', {
description: 'Execute many native desktop actions locally on the device in one round trip. Use this for fast deterministic UI workflows.',
inputSchema: withDevice({
steps: z.array(z.record(z.string(), z.any())).min(1).max(100),
continue_on_error: z.boolean().optional(),
timeout_ms: z.number().int().min(500).max(180000).optional()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, steps, continue_on_error, timeout_ms }) =>
jsonText(await rpc(device, 'native_call', {
action: 'batch',
args: { steps, continueOnError: !!continue_on_error },
timeoutMs: timeout_ms || 60000
}, (timeout_ms || 60000) + 3000)));
mcp.registerTool('desktop_batch', {
description: 'FAST PATH: execute many native desktop actions locally in one round trip, then return one final screenshot. Prefer this over separate mouse/keyboard/screenshot calls for interactive workflows.',
inputSchema: withDevice({
steps: z.array(z.record(z.string(), z.any())).min(1).max(100),
continue_on_error: z.boolean().optional(),
timeout_ms: z.number().int().min(500).max(180000).optional(),
screenshot: z.record(z.string(), z.any()).optional()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, steps, continue_on_error, timeout_ms, screenshot }) =>
normalizeResult(await rpc(device, 'native_batch_frame', {
steps, continueOnError: !!continue_on_error,
timeoutMs: timeout_ms || 60000,
screenshot: screenshot || {}
}, (timeout_ms || 60000) + 33000)));
mcp.registerTool('computer_operation_status',{
description:'Read the persistent outcome of a guarded device operation. A running or unknown outcome MUST NOT be automatically replayed after reconnect.',
inputSchema:withDevice({idempotency_key:z.string().min(8).max(128)}),
annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
},async ({device,idempotency_key})=>{
const d=getDevice(device,'control'),user=currentUser();
const operationId='op_'+crypto.createHash('sha256').update(idempotency_key).digest('hex').slice(0,64);
const row=operationJournal.get(user.id,d.id,operationId);
let deviceOutcome=null;
if(row&&row.state!=='completed'){
const actionId='a_'+crypto.createHash('sha256').update(JSON.stringify([user.id,d.id,operationId])).digest('hex');
try{deviceOutcome=await rpc(d.id,'guarded_action_status',{actionId},10000);}
catch{deviceOutcome={state:'unavailable'};}
}
return jsonText(row?{state:row.state,outcome:row.state==='completed'?JSON.parse(row.outcome):null,
deviceOutcome,
nextStep:row.state==='running'||row.state==='needs_review'?'Inspect current device state; do not replay a mutation.':'No automatic action taken.'}:
{state:'not_found',outcome:null,nextStep:'No recorded operation under this account/device/key.'});
});
mcp.registerTool('computer_observe', {
description: 'EXPERIMENTAL GUARDED EXECUTION: observe current foreground window and return preview plus an opaque observationId. Use computer_act with that exact ID; if pixels, window or geometry change the action is rejected as stale. Existing direct tools remain available.',
inputSchema: withDevice({
semantic:z.boolean().optional(),
quality:z.number().int().min(20).max(95).optional(),
maxWidth:z.number().int().min(240).max(1920).optional(),
maxHeight:z.number().int().min(180).max(1200).optional()
}),annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
},async ({device,...params}) => normalizeResult(await execution.observe({
userId:currentUser().id,deviceId:getDevice(device,'control').id,params
})));
mcp.registerTool('computer_act', {
description: 'EXPERIMENTAL GUARDED ACTION: perform one preview click from a recent computer_observe frame. Requires observation_id and a unique idempotency_key. The native agent rechecks foreground, bounds and pixels atomically before clicking; never retry a timed-out mutation with a different key. Success confirms input delivery, not a goal unless an explicit postcondition is verified.',
inputSchema: withDevice({
observation_id:z.string().min(1),
idempotency_key:z.string().min(8).max(128),
x:z.number().int().min(0),y:z.number().int().min(0),
verify_title_contains:z.string().min(1).max(120).optional()
}),annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
},async ({device,observation_id,idempotency_key,x,y,verify_title_contains}) =>
jsonText(await durableComputerAction(device,idempotency_key,'computer_act',({observation_id,x,y,verify_title_contains}),
({userId,deviceId,actionId})=>execution.act({userId,deviceId,actionId,observationId:observation_id,
idempotencyKey:idempotency_key,x,y,
verify:verify_title_contains?{kind:'window_title_contains',value:verify_title_contains}:undefined}))))
mcp.registerTool('computer_semantic_act', {
description: 'EXPERIMENTAL GUARDED UIA: execute bounded exact Windows UIA assert/set/invoke steps with mandatory postconditions from a fresh computer_observe frame. The native agent validates the frame before any side effect; ambiguous effects are never auto-replayed.',
inputSchema: withDevice({
observation_id:z.string().min(1),
idempotency_key:z.string().min(8).max(128),
steps:z.array(z.record(z.string(),z.any())).min(1).max(20)
}),annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
},async ({device,observation_id,idempotency_key,steps}) =>
jsonText(await durableComputerAction(device,idempotency_key,'computer_semantic_act',({observation_id,steps}),
({userId,deviceId,actionId})=>execution.act({userId,deviceId,actionId,observationId:observation_id,
idempotencyKey:idempotency_key,mode:'semantic',steps}))))
mcp.registerTool('computer_execute', {
description: 'EXPERIMENTAL AUTO ROUTER: given a recent computer_observe frame, prefer uniquely evidenced UIA invoke with an exact postcondition. Pixel fallback is allowed only with explicit allow_pixel_fallback and valid preview coordinates. Idempotency and stale-reference guards apply to either route.',
inputSchema: withDevice({
observation_id:z.string().min(1),
idempotency_key:z.string().min(8).max(128),
selector:z.record(z.string(),z.any()).optional(),
expect:z.record(z.string(),z.any()).optional(),
x:z.number().int().min(0).optional(),y:z.number().int().min(0).optional(),
allow_pixel_fallback:z.boolean().optional()
}),annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
},async ({device,observation_id,idempotency_key,selector,expect,x,y,allow_pixel_fallback}) =>
jsonText(await durableComputerAction(device,idempotency_key,'computer_execute',({observation_id,selector,expect,x,y,allow_pixel_fallback}),
({userId,deviceId,actionId})=>execution.autoAct({userId,deviceId,actionId,observationId:observation_id,
idempotencyKey:idempotency_key,selector,expect,x,y,
allowPixelFallback:allow_pixel_fallback===true}))))
mcp.registerTool('computer_browser_observe', {
description: 'Guarded BACKGROUND browser observation. Supply an explicit CDP tab ID (from browser_tabs) and a CSS selector. Return a private observationId bound to account, device, tab session, URL and exact element state. Does not require the Windows foreground or start a browser automatically.',
inputSchema:withDevice({
tab_id:z.string().min(1),selector:z.string().min(1).max(256)
}),annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
},async ({device,tab_id,selector}) =>
jsonText(await browserExecution.observe({userId:currentUser().id,deviceId:getDevice(device,'control').id,
tabId:tab_id,selector})));
mcp.registerTool('computer_browser_act', {
description: 'Guarded BACKGROUND CDP click/type on one previously observed unique non-password DOM element. Validates tab identity, URL and element state in the browser before input. Uses a one-shot observationId and idempotency_key; an uncertain mutation is never retried automatically.',
inputSchema:withDevice({
observation_id:z.string().min(1),idempotency_key:z.string().min(8).max(128),
selector:z.string().min(1).max(256),kind:z.enum(['click','type']),
text:z.string().max(20000).optional(),
expect_url_contains:z.string().min(1).max(256).optional(),
expect_text_contains:z.string().min(1).max(256).optional()
}),annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
},async ({device,observation_id,idempotency_key,selector,kind,text,expect_url_contains,expect_text_contains}) =>
jsonText(await durableComputerAction(device,idempotency_key,'computer_browser_act',({observation_id,selector,kind,text,expect_url_contains,expect_text_contains}),
({userId,deviceId,actionId})=>browserExecution.act({userId,deviceId,actionId,observationId:observation_id,
idempotencyKey:idempotency_key,selector,kind,text,
expectUrlContains:expect_url_contains,expectTextContains:expect_text_contains}))))
// Every route first checks the actual session; a locked/secure desktop is
// NEVER treated as a failed mouse click that should be retried with focus.
async function currentCapabilities(deviceId){
const [ss,si,br]=await Promise.allSettled([
rpc(deviceId,'native_call',{action:'session_state',args:{}},10000),
rpc(deviceId,'system_info',{},10000),
rpc(deviceId,'browser_call',{action:'status',args:{}},10000)
]);
return {session:ss.status==='fulfilled'?(ss.value?.result||{}):{},
agent:si.status==='fulfilled'?(si.value||{}):{},
browser:br.status==='fulfilled'?(br.value||{}):{}};
}
mcp.registerTool('computer_plan', {
description:'Choose a verified execution route for Windows, including locked RDP sessions. Never attempt GUI/UAC input when no interactive desktop; prefer authorized filesystem, process or isolated browser background execution.',
inputSchema:withDevice({kind:z.enum(['file_read','file_write','process_run','browser','desktop']),
requires_admin:z.boolean().optional(),allow_browser_start:z.boolean().optional(),
alternate_kind:z.enum(['file_read','file_write','process_run','browser']).optional()}),
annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
},async ({device,kind,requires_admin,allow_browser_start,alternate_kind})=>{
const d=getDevice(device,'control'),caps=await currentCapabilities(d.id);
const plan=chooseRoute({kind,session:caps.session,
agent:{...caps.agent,elevated:caps.session.elevated===true,
uia:caps.session.interactive===true&&caps.session.locked===false&&caps.agent.nativeHelper===true},
browser:caps.browser,requiresAdmin:requires_admin===true,
allowBrowserStart:allow_browser_start===true,alternate:alternate_kind?{kind:alternate_kind}:null});
return jsonText({kind,plan,checks:{sessionLocked:caps.session.locked??null,
approvalPending:caps.session.approvalPending===true,
interactive:caps.session.interactive===true,elevated:caps.session.elevated===true,
directFilesystem:caps.agent.directFilesystem===true,
directProcesses:caps.agent.directProcessSessions===true,
browserCdpRunning:caps.browser.running===true},
recommendedTools:plan.ready?
plan.route==='background.filesystem'?['fs_stat','fs_list','fs_read_text','fs_write_text']:
plan.route==='background.process'?['exec_command','process_start','process_read']:
plan.route.startsWith('browser.')?['browser_tabs','computer_browser_observe','computer_browser_act']:
['computer_observe','computer_execute','computer_semantic_act','computer_act']:
[]});
});
mcp.registerTool('computer_background_task', {
description:'Execute a bounded authorized task without mouse or keyboard, even when Windows is locked. File reading works in background; process execution requires a fresh idempotency key, writes an operation journal on server AND agent, checks requested Administrator status, and never retries uncertain effects.',
inputSchema:withDevice({
kind:z.enum(['file_read','process_run']),
path:z.string().min(1).max(2048).optional(),
command:z.string().min(1).max(10000).optional(),
shell:z.enum(['powershell','cmd']).optional(),
timeout_ms:z.number().int().min(1000).max(120000).optional(),
requires_admin:z.boolean().optional(),
idempotency_key:z.string().min(8).max(128).optional()
}),annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
},async ({device,kind,path,command,shell,timeout_ms,requires_admin,idempotency_key})=>{
const d=getDevice(device,'control'),caps=await currentCapabilities(d.id);
const plan=chooseRoute({kind,session:caps.session,
agent:{...caps.agent,elevated:caps.session.elevated===true},
browser:caps.browser,requiresAdmin:requires_admin===true});
if(!plan.ready)return jsonText({status:'not_ready',effect:'rejected',plan});
if(kind==='file_read'){
if(!path||command||idempotency_key)return jsonText({status:'invalid',error:'Provide path only for file_read'});
const data=await rpc(d.id,'fs_call',{action:'read_text',args:{path,maxBytes:512000}},15000);
return jsonText({status:'succeeded',routeUsed:plan.route,content:data});
}
if(!command||path||!idempotency_key)return jsonText({status:'invalid',error:'process_run requires command and unique idempotency_key'});
const result=await durableComputerAction(d.id,idempotency_key,'computer_background_task',
{kind,command,shell,timeout_ms,requires_admin},
({deviceId,actionId})=>rpc(deviceId,'exec',
{shell:shell||'powershell',command,timeoutMs:timeout_ms||30000,maxBytes:32000,
_yhActionId:actionId},(timeout_ms||30000)+8000));
return jsonText({routeUsed:plan.route,...(typeof result==='object'&&result?result:{result})});
});
mcp.registerTool('computer_doctor', {
description: 'Diagnose one of YOUR connected devices without modifying its state. Check remote session lock, native helper, OS/background capabilities, browser CDP status and RPC latency. Report which execution routes are currently available.',
inputSchema:withDevice(),annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
},async ({device}) => {
const d=getDevice(device,'control'),started=Date.now();
const [session,system,browser]=await Promise.allSettled([
rpc(d.id,'native_call',{action:'session_state',args:{}},10000),
rpc(d.id,'system_info',{},10000),
rpc(d.id,'browser_call',{action:'status',args:{}},10000)
]);
const state=session.status==='fulfilled'?session.value?.result||{}:null;
const info=system.status==='fulfilled'?system.value||{}:null;
const cd=browser.status==='fulfilled'?browser.value||{}:null;
const approvalPending=state?.approvalPending===true;
const interactive=state?.interactive===true&&state?.locked===false&&!approvalPending;
return jsonText({deviceId:d.id,agentVersion:d.meta.agentVersion||null,
checks:{agentConnected:true,nativeAvailable:session.status==='fulfilled',
sessionLocked:state?.locked??null,approvalPending,interactiveDesktop:interactive,
directFilesystem:info?.directFilesystem===true,
directProcesses:info?.directProcessSessions===true,
browserCdpRunning:cd?.running===true,
guardedFramesSupported:typeof info?.agentVersion==='string'&&
/^0\.9\.[1-9]/.test(info.agentVersion)&&session.status==='fulfilled',
elevated:state?.elevated===true,secureDesktopInputSupported:false,
worksWhenLocked:info?.directFilesystem===true||info?.directProcessSessions===true},
suggestedRoute:approvalPending?'wait_for_local_admin_approval':
interactive?'windows.uia_then_guarded_preview':
info?.directFilesystem||info?.directProcessSessions?'background_direct_api':'wait_for_interactive_session',
latencyMs:Date.now()-started});
});
mcp.registerTool('observe_desktop', {
description: 'PRIMARY FAST OBSERVATION: detect the current foreground/modal window, return a compact semantic UI snapshot and a lightweight window screenshot in one round trip. Prefer this before interacting with GUI or after an action may open a popup.',
inputSchema: withDevice({
hwnd: z.number().optional(),
autoRetarget: z.boolean().optional(),
semantic: z.boolean().optional(),
quality: z.number().int().min(20).max(95).optional(),
maxWidth: z.number().int().min(240).max(1920).optional(),
maxHeight: z.number().int().min(180).max(1200).optional()
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) =>
normalizeResult(await rpc(device, 'native_observe', {autoRetarget:true,...args}, 33000)));
mcp.registerTool('click_preview', {
description: 'Click using coordinates from the latest observe_desktop preview image. Coordinates are mapped automatically from preview pixels to the real foreground/modal window, avoiding scaling/DPI click errors.',
inputSchema: withDevice({
hwnd: z.number().optional(),
x: z.number().int().min(0),
y: z.number().int().min(0),
imageWidth: z.number().int().min(1),
imageHeight: z.number().int().min(1),
kind: z.enum(['click','double','move']).optional(),
button: z.enum(['left','right','middle']).optional(),
count: z.number().int().min(1).max(5).optional()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) =>
jsonText(await rpc(device, 'native_call', { action:'click_preview', args:{autoRetarget:true,...args} }, 15000)));
mcp.registerTool('semantic_snapshot', {
description: 'FAST semantic observation: return a compact Windows UI Automation control snapshot for one window without a screenshot. Use this to discover stable selectors before semantic_batch.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(),
title: z.string().optional(), titleContains: z.string().optional(),
requireForeground: z.boolean().optional()
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) =>
jsonText(await rpc(device, 'native_call', { action:'semantic_snapshot', args:{autoRetarget:true,...args} }, 30000)));
mcp.registerTool('semantic_batch', {
description: 'FAST semantic execution: run 1-20 UIA assert/set/invoke steps locally with exact postconditions and no automatic replay. Prefer for accessible forms/buttons/fields.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(),
title: z.string().optional(), titleContains: z.string().optional(),
requireForeground: z.boolean().optional(),
steps: z.array(z.record(z.string(), z.any())).min(1).max(20)
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, steps, ...scope }) =>
jsonText(await rpc(device, 'native_call', { action:'semantic_batch', args:{autoRetarget:true,...scope,steps}, timeoutMs:30000 }, 33000)));
mcp.registerTool('screenshot', {
description: 'Capture the full desktop or a specific window as a PNG image and return the actual image. For workflows that also click/type, prefer desktop_batch to avoid extra round trips.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(),
titleContains: z.string().optional(), window: z.boolean().optional(),
format: z.enum(['png','jpeg','jpg']).optional(),
quality: z.number().int().min(20).max(95).optional(),
maxWidth: z.number().int().min(240).max(7680).optional(),
maxHeight: z.number().int().min(180).max(4320).optional(),
keep: z.boolean().optional()
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => normalizeResult(await rpc(device, 'native_screenshot', args, 30000)));
mcp.registerTool('list_windows', {
description: 'List top-level Windows windows with HWND, PID, title, process and bounds.',
inputSchema: withDevice({ pid: z.number().int().optional(), visibleOnly: z.boolean().optional() }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'list_windows', args }, 15000)));
mcp.registerTool('foreground_window', {
description: 'Get the current foreground window.',
inputSchema: withDevice(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => jsonText(await rpc(device, 'native_call', { action:'foreground', args:{} }, 10000)));
mcp.registerTool('focus_window', {
description: 'Bring a window to the foreground by HWND, PID, exact title or title substring.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(),
title: z.string().optional(), titleContains: z.string().optional(), settleMs: z.number().int().optional()
}),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'focus_window', args }, 10000)));
mcp.registerTool('ui_tree', {
description: 'Read the Windows UI Automation tree directly, without screenshots.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(),
titleContains: z.string().optional(), limit: z.number().int().min(1).max(3000).optional()
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'ui_tree', args }, 30000)));
mcp.registerTool('ui_find', {
description: 'Find UI elements by name, partial name, automation id, control type or class name.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(), titleContains: z.string().optional(),
name: z.string().optional(), nameContains: z.string().optional(), automationId: z.string().optional(),
controlType: z.string().optional(), className: z.string().optional(),
limit: z.number().int().min(1).max(100).optional()
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'ui_find', args }, 30000)));
mcp.registerTool('ui_invoke', {
description: 'Invoke a UI Automation element directly; falls back to a native click on its bounds.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(), titleContains: z.string().optional(),
name: z.string().optional(), nameContains: z.string().optional(), automationId: z.string().optional(),
controlType: z.string().optional(), className: z.string().optional()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'ui_invoke', args }, 30000)));
mcp.registerTool('ui_set_value', {
description: 'Set the value of a UI Automation edit field directly; falls back to focus/select/type.',
inputSchema: withDevice({
hwnd: z.number().optional(), pid: z.number().int().optional(), titleContains: z.string().optional(),
name: z.string().optional(), nameContains: z.string().optional(), automationId: z.string().optional(),
controlType: z.string().optional(), className: z.string().optional(), value: z.string()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'ui_set_value', args }, 30000)));
mcp.registerTool('mouse', {
description: 'Move or click the native mouse immediately.',
inputSchema: withDevice({
x: z.number().int(), y: z.number().int(),
kind: z.enum(['move','click','double']).optional(),
button: z.enum(['left','right','middle']).optional(), count: z.number().int().min(1).max(5).optional()
}),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'mouse', args }, 10000)));
mcp.registerTool('scroll', {
description: 'Send native mouse-wheel input.',
inputSchema: withDevice({ x: z.number().int().optional(), y: z.number().int().optional(), delta: z.number().int().optional() }),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'scroll', args }, 10000)));
mcp.registerTool('hotkey', {
description: 'Send a native keyboard shortcut such as CTRL+S or ALT+F4.',
inputSchema: withDevice({ combo: z.string(), settleMs: z.number().int().optional() }),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'hotkey', args }, 10000)));
mcp.registerTool('type_text', {
description: 'Type Unicode text through native keyboard input.',
inputSchema: withDevice({ text: z.string(), delayMs: z.number().int().min(0).max(1000).optional() }),
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'type_text', args }, 30000)));
mcp.registerTool('clipboard_get', {
description: 'Read text from the Windows clipboard.',
inputSchema: withDevice(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device }) => jsonText(await rpc(device, 'native_call', { action:'clipboard_get', args:{} }, 10000)));
mcp.registerTool('clipboard_set', {
description: 'Set text on the Windows clipboard.',
inputSchema: withDevice({ text: z.string() }), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'clipboard_set', args }, 10000)));
mcp.registerTool('launch_app', {
description: 'Launch an application or document through Windows.',
inputSchema: withDevice({ file: z.string(), arguments: z.string().optional() }),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'launch', args }, 15000)));
mcp.registerTool('wait_window', {
description: 'Wait locally until a matching window appears, avoiding repeated AI polling.',
inputSchema: withDevice({
pid: z.number().int().optional(), title: z.string().optional(), titleContains: z.string().optional(),
timeoutMs: z.number().int().min(100).max(180000).optional(), pollMs: z.number().int().min(20).max(5000).optional()
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({ device, ...args }) => jsonText(await rpc(device, 'native_call', { action:'wait_window', args, timeoutMs: args.timeoutMs || 10000 }, (args.timeoutMs || 10000) + 3000)));
mcp.registerTool('browser_status', {
description: 'Check YourHand isolated Chromium CDP status on a device.',
inputSchema: withDevice(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device}) => jsonText(await rpc(device,'browser_call',{action:'status',args:{}},10000)));
mcp.registerTool('browser_start', {
description: 'Start or attach to YourHand isolated Chromium browser with local CDP control. Works without mouse/keyboard.',
inputSchema: withDevice({ url:z.string().optional(), timeout_ms:z.number().int().optional() }), annotations:{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
}, async ({device,url,timeout_ms}) => jsonText(await rpc(device,'browser_call',{action:'start',args:{url,timeoutMs:timeout_ms||10000}},(timeout_ms||10000)+3000)));
mcp.registerTool('browser_tabs', {
description: 'List controllable browser tabs/targets.',
inputSchema: withDevice(), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device}) => jsonText(await rpc(device,'browser_call',{action:'tabs',args:{}},10000)));
mcp.registerTool('browser_navigate', {
description: 'Navigate a CDP-controlled browser tab directly and wait for DOM readiness.',
inputSchema: withDevice({ url:z.string(), targetId:z.string().optional(), newTab:z.boolean().optional(), timeout_ms:z.number().int().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,url,targetId,newTab,timeout_ms}) => jsonText(await rpc(device,'browser_call',{action:'navigate',args:{url,targetId,newTab,timeoutMs:timeout_ms||15000}},(timeout_ms||15000)+3000)));
mcp.registerTool('browser_eval', {
description: 'Evaluate JavaScript in a browser tab through Chrome DevTools Protocol.',
inputSchema: withDevice({ expression:z.string(), targetId:z.string().optional(), timeout_ms:z.number().int().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,expression,targetId,timeout_ms}) => jsonText(await rpc(device,'browser_call',{action:'eval',args:{expression,targetId,timeoutMs:timeout_ms||15000}},(timeout_ms||15000)+3000)));
mcp.registerTool('browser_text', {
description: 'Read visible page text directly from the DOM.',
inputSchema: withDevice({ targetId:z.string().optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
}, async ({device,targetId}) => jsonText(await rpc(device,'browser_call',{action:'text',args:{targetId}},15000)));
mcp.registerTool('browser_click', {
description: 'Click a DOM element by CSS selector without screenshots or mouse movement.',
inputSchema: withDevice({ selector:z.string(), targetId:z.string().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,selector,targetId}) => jsonText(await rpc(device,'browser_call',{action:'click',args:{selector,targetId}},15000)));
mcp.registerTool('browser_type', {
description: 'Set an input value by CSS selector and dispatch input/change events directly in the DOM.',
inputSchema: withDevice({ selector:z.string(), text:z.string(), targetId:z.string().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,selector,text,targetId}) => jsonText(await rpc(device,'browser_call',{action:'type',args:{selector,text,targetId}},15000)));
mcp.registerTool('browser_wait_selector', {
description: 'Wait locally for a CSS selector to appear, avoiding repeated AI polling.',
inputSchema: withDevice({ selector:z.string(), targetId:z.string().optional(), timeout_ms:z.number().int().optional(), poll_ms:z.number().int().optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
}, async ({device,selector,targetId,timeout_ms,poll_ms}) => jsonText(await rpc(device,'browser_call',{action:'wait_selector',args:{selector,targetId,timeoutMs:timeout_ms||10000,pollMs:poll_ms||80}},(timeout_ms||10000)+3000)));
mcp.registerTool('browser_batch', {
description: 'Execute multiple browser/CDP actions locally in one round trip for high-speed deterministic workflows.',
inputSchema: withDevice({ steps:z.array(z.record(z.string(),z.any())).min(1).max(100), continue_on_error:z.boolean().optional(), timeout_ms:z.number().int().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,steps,continue_on_error,timeout_ms}) => jsonText(await rpc(device,'browser_batch',{steps,continueOnError:!!continue_on_error},(timeout_ms||60000))));
mcp.registerTool('browser_screenshot', {
description: 'Capture the browser page directly through CDP, including beyond the visible viewport.',
inputSchema: withDevice({ targetId:z.string().optional(), format:z.enum(['png','jpeg']).optional(), captureBeyondViewport:z.boolean().optional(), timeout_ms:z.number().int().optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
}, async ({device,...args}) => normalizeResult(await rpc(device,'browser_screenshot',{...args,timeoutMs:args.timeout_ms||30000},(args.timeout_ms||30000)+3000)));
mcp.registerTool('exec_command', {
description: 'Execute a one-shot PowerShell or cmd command directly through YourHand without Desktop Commander. Ideal for COM/API automation and fast local scripts.',
inputSchema: withDevice({
command:z.string(),
shell:z.enum(['powershell','cmd']).optional(),
cwd:z.string().optional(),
timeout_ms:z.number().int().min(500).max(300000).optional(),
max_bytes:z.number().int().min(1024).max(20971520).optional()
}),
annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,command,shell,cwd,timeout_ms,max_bytes}) => jsonText(await rpc(device,'exec',{command,shell,cwd,timeoutMs:timeout_ms||60000,maxBytes:max_bytes||5242880},(timeout_ms||60000)+5000)));
mcp.registerTool('fs_stat', {
description: 'Get native filesystem metadata directly from YourHand.',
inputSchema: withDevice({ path:z.string() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,path}) => jsonText(await rpc(device,'fs_call',{action:'stat',args:{path}},15000)));
mcp.registerTool('fs_list', {
description: 'List a directory directly through YourHand native filesystem.',
inputSchema: withDevice({ path:z.string() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,path}) => jsonText(await rpc(device,'fs_call',{action:'list',args:{path}},30000)));
mcp.registerTool('fs_read_text', {
description: 'Read a text file directly with offset and bounded byte length.',
inputSchema: withDevice({ path:z.string(), offset:z.number().int().min(0).optional(), length:z.number().int().min(0).optional(), max_bytes:z.number().int().min(1).max(20971520).optional(), encoding:z.string().optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,path,offset,length,max_bytes,encoding}) => jsonText(await rpc(device,'fs_call',{action:'read_text',args:{path,offset,length,maxBytes:max_bytes,encoding}},30000)));
mcp.registerTool('fs_read_binary', {
description: 'Read a bounded binary file directly as base64.',
inputSchema: withDevice({ path:z.string(), max_bytes:z.number().int().min(1).max(20971520).optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,path,max_bytes}) => jsonText(await rpc(device,'fs_call',{action:'read_binary',args:{path,maxBytes:max_bytes}},30000)));
mcp.registerTool('fs_write_text', {
description: 'Create, overwrite or append a text file directly through YourHand.',
inputSchema: withDevice({ path:z.string(), text:z.string(), append:z.boolean().optional(), encoding:z.string().optional(), create_parents:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,path,text,append,encoding,create_parents}) => jsonText(await rpc(device,'fs_call',{action:'write_text',args:{path,text,append,encoding,createParents:create_parents!==false}},30000)));
mcp.registerTool('fs_write_binary', {
description: 'Create, overwrite or append a binary file from base64 directly through YourHand.',
inputSchema: withDevice({ path:z.string(), base64:z.string(), append:z.boolean().optional(), create_parents:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,path,base64,append,create_parents}) => jsonText(await rpc(device,'fs_call',{action:'write_binary',args:{path,base64,append,createParents:create_parents!==false}},30000)));
mcp.registerTool('fs_mkdir', {
description: 'Create a directory directly through YourHand.',
inputSchema: withDevice({ path:z.string(), recursive:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,path,recursive}) => jsonText(await rpc(device,'fs_call',{action:'mkdir',args:{path,recursive:recursive!==false}},15000)));
mcp.registerTool('fs_move', {
description: 'Move or rename a filesystem item directly through YourHand.',
inputSchema: withDevice({ path:z.string(), destination:z.string(), create_parents:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,path,destination,create_parents}) => jsonText(await rpc(device,'fs_call',{action:'move',args:{path,destination,createParents:create_parents!==false}},30000)));
mcp.registerTool('fs_copy', {
description: 'Copy a file or directory directly through YourHand.',
inputSchema: withDevice({ path:z.string(), destination:z.string(), recursive:z.boolean().optional(), force:z.boolean().optional(), create_parents:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,path,destination,recursive,force,create_parents}) => jsonText(await rpc(device,'fs_call',{action:'copy',args:{path,destination,recursive:!!recursive,force:force!==false,createParents:create_parents!==false}},60000)));
mcp.registerTool('fs_remove', {
description: 'Remove a file or directory directly through YourHand.',
inputSchema: withDevice({ path:z.string(), recursive:z.boolean().optional(), force:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,path,recursive,force}) => jsonText(await rpc(device,'fs_call',{action:'remove',args:{path,recursive:!!recursive,force:!!force}},30000)));
mcp.registerTool('fs_search', {
description: 'Search filenames/paths recursively on the device without Desktop Commander.',
inputSchema: withDevice({ path:z.string(), query:z.string().optional(), max_results:z.number().int().min(1).max(5000).optional(), max_depth:z.number().int().min(0).max(64).optional(), include_dirs:z.boolean().optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,path,query,max_results,max_depth,include_dirs}) => jsonText(await rpc(device,'fs_call',{action:'search',args:{path,query,maxResults:max_results,maxDepth:max_depth,includeDirs:!!include_dirs}},65000)));
mcp.registerTool('process_start', {
description: 'Start a native YourHand-managed process session. Interactive sessions keep stdin/stdout open for later calls.',
inputSchema: withDevice({ command:z.string().optional(), shell:z.enum(['powershell','cmd']).optional(), interactive:z.boolean().optional(), executable:z.string().optional(), arguments:z.array(z.string()).optional(), cwd:z.string().optional(), show_window:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,command,shell,interactive,executable,arguments:argv,cwd,show_window}) => jsonText(await rpc(device,'proc_call',{action:'start',args:{command,shell,interactive,executable,arguments:argv,cwd,showWindow:!!show_window}},15000)));
mcp.registerTool('process_sessions', {
description: 'List YourHand-managed process sessions.',
inputSchema: withDevice(), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device}) => jsonText(await rpc(device,'proc_call',{action:'list',args:{}},10000)));
mcp.registerTool('process_read', {
description: 'Read incremental buffered stdout/stderr from a YourHand process session.',
inputSchema: withDevice({ session_id:z.string(), offset:z.number().int().min(0).optional() }), annotations:{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
}, async ({device,session_id,offset}) => jsonText(await rpc(device,'proc_call',{action:'read',args:{sessionId:session_id,offset}},15000)));
mcp.registerTool('process_write', {
description: 'Write stdin to a YourHand interactive process session.',
inputSchema: withDevice({ session_id:z.string(), input:z.string() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
}, async ({device,session_id,input}) => jsonText(await rpc(device,'proc_call',{action:'write',args:{sessionId:session_id,input}},15000)));
mcp.registerTool('process_kill', {
description: 'Terminate a YourHand-managed process session.',
inputSchema: withDevice({ session_id:z.string(), signal:z.string().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,session_id,signal}) => jsonText(await rpc(device,'proc_call',{action:'kill',args:{sessionId:session_id,signal}},15000)));
mcp.registerTool('process_close_stdin', {
description: 'Close stdin for a YourHand-managed process session.',
inputSchema: withDevice({ session_id:z.string() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,session_id}) => jsonText(await rpc(device,'proc_call',{action:'close_stdin',args:{sessionId:session_id}},10000)));
mcp.registerTool('process_forget', {
description: 'Forget a completed YourHand process session and release its buffered history.',
inputSchema: withDevice({ session_id:z.string(), force:z.boolean().optional() }), annotations:{ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
}, async ({device,session_id,force}) => jsonText(await rpc(device,'proc_call',{action:'forget',args:{sessionId:session_id,force:!!force}},10000)));
return mcp;
}
// The SDK helper installs Express' default 100 KB JSON parser; large MCP tool calls need a bounded limit.
const express = require('express');
const { hostHeaderValidation } = require(path.join(path.dirname(require.resolve('@modelcontextprotocol/sdk/server/express.js')), 'middleware', 'hostHeaderValidation.js'));
const mcpApp = express();
mcpApp.use(hostHeaderValidation(['127.0.0.1', 'localhost', 'app.wolvexai.com', 'yourhand.wolvexai.com']));
const mcpEndpoints = ['/mcp', '/yourhand-rpc', ...(LEGACY_WEB_MCP_PATH ? [LEGACY_WEB_MCP_PATH] : [])];
mcpApp.use(mcpEndpoints, requireMcpAuth);
mcpApp.use(mcpEndpoints, express.json({ limit: '32mb' }));
mcpApp.use((err, _req, res, next) => {
if (err?.type === 'entity.too.large') return res.status(413).json({ jsonrpc:'2.0', error:{ code:-32000, message:'MCP request exceeds 32 MB limit' }, id:null });
if (err?.type === 'entity.parse.failed') return res.status(400).json({ jsonrpc:'2.0', error:{ code:-32700, message:'Invalid JSON body' }, id:null });
next(err);
});
mcpApp.get('/health', (_req, res) => {
res.json({ ok: true, name: PRODUCT_NAME, version: VERSION });
});
async function handleMcpRequest(req, res) {
return userContext.run(req.yhUser, async () => {
const mcp = makeMcpServer();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
try {
await mcp.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('[YourHand MCP] request failed:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: 'Internal server error' },
id: null
});
}
} finally {
res.on('close', () => {
try { transport.close(); } catch {}
try { mcp.close(); } catch {}
});
}
});
}
mcpApp.post('/mcp', handleMcpRequest);
mcpApp.post('/yourhand-rpc', handleMcpRequest);
if (LEGACY_WEB_MCP_PATH) mcpApp.post(LEGACY_WEB_MCP_PATH, handleMcpRequest);
const methodNotAllowed = (_req, res) => res.status(405).json({
jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null
});
mcpApp.get('/mcp', methodNotAllowed);
mcpApp.delete('/mcp', methodNotAllowed);
mcpApp.get('/yourhand-rpc', methodNotAllowed);
mcpApp.delete('/yourhand-rpc', methodNotAllowed);
if (LEGACY_WEB_MCP_PATH) {
mcpApp.get(LEGACY_WEB_MCP_PATH, methodNotAllowed);
mcpApp.delete(LEGACY_WEB_MCP_PATH, methodNotAllowed);
}
const mcpHttp = http.createServer(mcpApp);
mcpHttp.listen(MCP_PORT, '127.0.0.1', () => {
console.log(`YourHand MCP ${VERSION} on http://127.0.0.1:${MCP_PORT}/mcp`);
});
// This Web dashboard probe runs without an MCP OAuth context, and never
// reserves a mutating GUI lock or issues a side effect to a shared desktop.
function probeDeviceFromWeb(userId,deviceId){
const telemetryStarted=performance.now(),traceId=crypto.randomUUID();
const done=(ok,error)=>telemetry.record({traceId,userId,deviceId,eventType:'web_connect',
methodId:'WEB_DEVICE_CONNECT',routeId:'agent.websocket',attemptIndex:1,methodsTried:1,
durationMs:performance.now()-telemetryStarted,outcome:ok?'completed':'failed',
errorCode:error,errorStage:ok?null:'WEB_CONNECT'});
if(!store.getDeviceForUser(userId,deviceId)){done(false,'ACCESS_DENIED');throw Error('DEVICE_NOT_AUTHORIZED');}
const d=devices.get(deviceId);
if(!d||d.ws.readyState!==1||!store.getActiveDevice(deviceId))
{done(false,'CONNECTION_LOST');throw Error('AGENT_OFFLINE');}
const started=Date.now(),id=crypto.randomUUID(),socket=d.ws;
return new Promise((resolve,reject)=>{
const timer=setTimeout(()=>{
if(pending.get(id)?.ws===socket)pending.delete(id);
reject(Error('Agent command timeout'));
},8000);
pending.set(id,{resolve:reply=>resolve({pong:reply?.pong===true,latencyMs:Date.now()-started}),
reject,timer,deviceId,ws:socket,requesterId:userId});
try{
socket.send(JSON.stringify({type:'request',id,method:'ping',params:{}}),err=>{
if(err&&pending.get(id)?.ws===socket){
pending.delete(id);clearTimeout(timer);reject(err);
}
});
}catch(err){pending.delete(id);clearTimeout(timer);reject(err)}
}).then(result=>{if(result.pong!==true)throw Error('Unexpected Agent ping response');done(true);return result},error=>{done(false,error?.message);throw error});
}
const webApp = createWebApp({
store,
oauth,
usageMeter,
baseUrl: BASE_URL,
googleClientId: process.env.GOOGLE_CLIENT_ID || '',
webDir: WEB_DIR,
isDeviceConnected: deviceId => { const d=devices.get(deviceId); return Boolean(d && d.ws.readyState===1 && d.userId && store.getActiveDevice(deviceId)); },
telemetry,
probeDevice:probeDeviceFromWeb,
onDeviceRevoked: deviceId => {
const live = devices.get(deviceId);
if (!live) return;
dropDevice(deviceId, live.ws, 'Unpaired by owner');
try { live.ws.close(1008, 'Unpaired by owner'); } catch {}
}
});
const webHttp = http.createServer(webApp);
webHttp.listen(WEB_PORT, '127.0.0.1', () => {
console.log(`YourHand Web ${VERSION} on ${BASE_URL}`);
});
agentHttp.listen(AGENT_PORT, '127.0.0.1', () => {
console.log(`YourHand Agent ${VERSION} on http://127.0.0.1:${AGENT_PORT}/agent`);
});
function shutdown() {
for (const d of devices.values()) {
try { d.ws.close(1001, 'Server shutdown'); } catch {}
}
try { wss.close(); } catch {}
try { agentHttp.close(); } catch {}
try { mcpHttp.close(); } catch {}
try { webHttp.close(); } catch {}
setTimeout(() => process.exit(0), 500).unref();
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
process.on('uncaughtException', err => console.error('[fatal]', err));
process.on('unhandledRejection', err => console.error('[rejection]', err));