Ctrl K
ComunidadGuías…

server-multiuser.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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));