@midscene/computer 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { BaseMCPServer, BaseMidsceneTools, createMCPServerLauncher } from "@midscene/shared/mcp";
2
2
  import { Agent } from "@midscene/core/agent";
3
3
  import node_assert from "node:assert";
4
- import { execSync } from "node:child_process";
4
+ import { execSync, spawn } from "node:child_process";
5
5
  import { createRequire } from "node:module";
6
6
  import { getMidsceneLocationSchema, z } from "@midscene/core";
7
7
  import { actionHoverParamSchema, defineAction, defineActionClearInput, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionRightClick, defineActionScroll, defineActionTap } from "@midscene/core/device";
@@ -9,6 +9,74 @@ import { sleep } from "@midscene/core/utils";
9
9
  import { createImgBase64ByFormat } from "@midscene/shared/img";
10
10
  import { getDebug } from "@midscene/shared/logger";
11
11
  import screenshot_desktop from "screenshot-desktop";
12
+ import { existsSync } from "node:fs";
13
+ const debugXvfb = getDebug('computer:xvfb');
14
+ function checkXvfbInstalled() {
15
+ try {
16
+ execSync('which Xvfb', {
17
+ stdio: 'ignore'
18
+ });
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ function findAvailableDisplay(startFrom = 99) {
25
+ for(let n = startFrom; n < startFrom + 100; n++)if (!existsSync(`/tmp/.X${n}-lock`)) return n;
26
+ throw new Error(`No available display number found (checked ${startFrom} to ${startFrom + 99})`);
27
+ }
28
+ function startXvfb(options) {
29
+ const resolution = options?.resolution || '1920x1080x24';
30
+ const displayNum = options?.displayNumber ?? findAvailableDisplay();
31
+ const display = `:${displayNum}`;
32
+ return new Promise((resolve, reject)=>{
33
+ debugXvfb(`Starting Xvfb on display ${display} with resolution ${resolution}`);
34
+ const xvfbProcess = spawn('Xvfb', [
35
+ display,
36
+ '-screen',
37
+ '0',
38
+ resolution,
39
+ '-ac',
40
+ '-nolisten',
41
+ 'tcp'
42
+ ], {
43
+ stdio: 'ignore'
44
+ });
45
+ let settled = false;
46
+ xvfbProcess.on('error', (err)=>{
47
+ if (!settled) {
48
+ settled = true;
49
+ reject(new Error(`Failed to start Xvfb: ${err.message}`));
50
+ }
51
+ });
52
+ xvfbProcess.on('exit', (code)=>{
53
+ if (!settled) {
54
+ settled = true;
55
+ reject(new Error(`Xvfb exited unexpectedly with code ${code}`));
56
+ }
57
+ });
58
+ const instance = {
59
+ process: xvfbProcess,
60
+ display,
61
+ stop () {
62
+ try {
63
+ xvfbProcess.kill('SIGTERM');
64
+ } catch {}
65
+ }
66
+ };
67
+ setTimeout(()=>{
68
+ if (!settled) {
69
+ settled = true;
70
+ debugXvfb(`Xvfb started on display ${display}`);
71
+ resolve(instance);
72
+ }
73
+ }, 500);
74
+ });
75
+ }
76
+ function needsXvfb(explicitOpt) {
77
+ if ('linux' !== process.platform) return false;
78
+ return true === explicitOpt;
79
+ }
12
80
  function _define_property(obj, key, value) {
13
81
  if (key in obj) Object.defineProperty(obj, key, {
14
82
  value: value,
@@ -179,53 +247,74 @@ class ComputerDevice {
179
247
  async connect() {
180
248
  debugDevice('Connecting to computer device');
181
249
  try {
250
+ const headless = this.options?.headless ?? 'true' === process.env.MIDSCENE_COMPUTER_HEADLESS_LINUX;
251
+ if (needsXvfb(headless)) {
252
+ if (!checkXvfbInstalled()) throw new Error('Xvfb is required for headless mode but not installed. Install: sudo apt-get install xvfb');
253
+ this.xvfbInstance = await startXvfb({
254
+ resolution: this.options?.xvfbResolution
255
+ });
256
+ process.env.DISPLAY = this.xvfbInstance.display;
257
+ debugDevice(`Xvfb started on display ${this.xvfbInstance.display}`);
258
+ this.xvfbCleanup = ()=>{
259
+ if (this.xvfbInstance) {
260
+ this.xvfbInstance.stop();
261
+ this.xvfbInstance = void 0;
262
+ }
263
+ };
264
+ process.on('exit', this.xvfbCleanup);
265
+ process.on('SIGINT', this.xvfbCleanup);
266
+ process.on('SIGTERM', this.xvfbCleanup);
267
+ }
182
268
  libnut = await getLibnut();
183
269
  const size = await this.size();
184
270
  const displays = await ComputerDevice.listDisplays();
271
+ const headlessInfo = this.xvfbInstance ? `\nHeadless: true (Xvfb on ${this.xvfbInstance.display})` : '';
185
272
  this.description = `
186
273
  Type: Computer
187
274
  Platform: ${process.platform}
188
275
  Display: ${this.displayId || 'Primary'}
189
276
  Screen Size: ${size.width}x${size.height}
190
- Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}
277
+ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}${headlessInfo}
191
278
  `;
192
279
  debugDevice('Computer device connected', this.description);
280
+ await this.healthCheck();
193
281
  } catch (error) {
282
+ if (this.xvfbInstance) {
283
+ this.xvfbInstance.stop();
284
+ this.xvfbInstance = void 0;
285
+ }
194
286
  debugDevice(`Failed to connect: ${error}`);
195
287
  throw new Error(`Unable to connect to computer device: ${error}`);
196
288
  }
197
- await this.healthCheck();
198
289
  }
199
290
  async healthCheck() {
200
291
  console.log('[HealthCheck] Starting health check...');
201
292
  console.log('[HealthCheck] Taking screenshot...');
202
- try {
203
- const base64 = await this.screenshotBase64();
204
- console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
205
- } catch (error) {
206
- console.error(`[HealthCheck] Screenshot failed: ${error}`);
207
- process.exit(1);
208
- }
293
+ const screenshotTimeout = 15000;
294
+ let timeoutId;
295
+ const timeoutPromise = new Promise((_, reject)=>{
296
+ timeoutId = setTimeout(()=>reject(new Error('Screenshot timed out')), screenshotTimeout);
297
+ });
298
+ const base64 = await Promise.race([
299
+ this.screenshotBase64().finally(()=>clearTimeout(timeoutId)),
300
+ timeoutPromise
301
+ ]);
302
+ console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
209
303
  console.log('[HealthCheck] Moving mouse...');
210
- try {
211
- node_assert(libnut, 'libnut not initialized');
212
- const startPos = libnut.getMousePos();
213
- console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
214
- const offsetX = Math.floor(40 * Math.random()) + 10;
215
- const offsetY = Math.floor(40 * Math.random()) + 10;
216
- const targetX = startPos.x + offsetX;
217
- const targetY = startPos.y + offsetY;
218
- console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
219
- libnut.moveMouse(targetX, targetY);
220
- await sleep(50);
221
- const movedPos = libnut.getMousePos();
222
- console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
223
- libnut.moveMouse(startPos.x, startPos.y);
224
- console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
225
- } catch (error) {
226
- console.error(`[HealthCheck] Mouse move failed: ${error}`);
227
- process.exit(1);
228
- }
304
+ node_assert(libnut, 'libnut not initialized');
305
+ const startPos = libnut.getMousePos();
306
+ console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
307
+ const offsetX = Math.floor(40 * Math.random()) + 10;
308
+ const offsetY = Math.floor(40 * Math.random()) + 10;
309
+ const targetX = startPos.x + offsetX;
310
+ const targetY = startPos.y + offsetY;
311
+ console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
312
+ libnut.moveMouse(targetX, targetY);
313
+ await sleep(50);
314
+ const movedPos = libnut.getMousePos();
315
+ console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
316
+ libnut.moveMouse(startPos.x, startPos.y);
317
+ console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
229
318
  console.log('[HealthCheck] Health check passed');
230
319
  }
231
320
  async screenshotBase64() {
@@ -255,8 +344,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
255
344
  const screenSize = libnut.getScreenSize();
256
345
  return {
257
346
  width: screenSize.width,
258
- height: screenSize.height,
259
- dpr: 1
347
+ height: screenSize.height
260
348
  };
261
349
  } catch (error) {
262
350
  debugDevice(`Failed to get screen size: ${error}`);
@@ -521,6 +609,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
521
609
  }
522
610
  async destroy() {
523
611
  if (this.destroyed) return;
612
+ if (this.xvfbInstance) {
613
+ this.xvfbInstance.stop();
614
+ this.xvfbInstance = void 0;
615
+ }
616
+ if (this.xvfbCleanup) {
617
+ process.removeListener('exit', this.xvfbCleanup);
618
+ process.removeListener('SIGINT', this.xvfbCleanup);
619
+ process.removeListener('SIGTERM', this.xvfbCleanup);
620
+ this.xvfbCleanup = void 0;
621
+ }
524
622
  this.destroyed = true;
525
623
  debugDevice('Computer device destroyed');
526
624
  }
@@ -533,6 +631,8 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
533
631
  _define_property(this, "displayId", void 0);
534
632
  _define_property(this, "description", void 0);
535
633
  _define_property(this, "destroyed", false);
634
+ _define_property(this, "xvfbInstance", void 0);
635
+ _define_property(this, "xvfbCleanup", void 0);
536
636
  _define_property(this, "useAppleScript", void 0);
537
637
  _define_property(this, "uri", void 0);
538
638
  this.options = options;
@@ -561,7 +661,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
561
661
  createTemporaryDevice() {
562
662
  return new ComputerDevice({});
563
663
  }
564
- async ensureAgent(displayId) {
664
+ async ensureAgent(displayId, headless) {
565
665
  if (this.agent && displayId) {
566
666
  try {
567
667
  await this.agent.destroy?.();
@@ -572,10 +672,15 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
572
672
  }
573
673
  if (this.agent) return this.agent;
574
674
  debug('Creating Computer agent with displayId:', displayId || 'primary');
575
- const opts = displayId ? {
576
- displayId
577
- } : void 0;
578
- const agent = await agentFromComputer(opts);
675
+ const opts = {
676
+ ...displayId ? {
677
+ displayId
678
+ } : {},
679
+ ...void 0 !== headless ? {
680
+ headless
681
+ } : {}
682
+ };
683
+ const agent = await agentFromComputer(Object.keys(opts).length > 0 ? opts : void 0);
579
684
  this.agent = agent;
580
685
  return agent;
581
686
  }
@@ -585,10 +690,11 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
585
690
  name: 'computer_connect',
586
691
  description: 'Connect to computer desktop. Provide displayId to connect to a specific display (use computer_list_displays to get available IDs). If not provided, uses the primary display.',
587
692
  schema: {
588
- displayId: z.string().optional().describe('Display ID (from computer_list_displays)')
693
+ displayId: z.string().optional().describe('Display ID (from computer_list_displays)'),
694
+ headless: z.boolean().optional().describe('Start virtual display via Xvfb (Linux only)')
589
695
  },
590
- handler: async ({ displayId })=>{
591
- const agent = await this.ensureAgent(displayId);
696
+ handler: async ({ displayId, headless })=>{
697
+ const agent = await this.ensureAgent(displayId, headless);
592
698
  const screenshot = await agent.interface.screenshotBase64();
593
699
  return {
594
700
  content: [
package/dist/lib/cli.js CHANGED
@@ -38,6 +38,74 @@ const utils_namespaceObject = require("@midscene/core/utils");
38
38
  const img_namespaceObject = require("@midscene/shared/img");
39
39
  const external_screenshot_desktop_namespaceObject = require("screenshot-desktop");
40
40
  var external_screenshot_desktop_default = /*#__PURE__*/ __webpack_require__.n(external_screenshot_desktop_namespaceObject);
41
+ const external_node_fs_namespaceObject = require("node:fs");
42
+ const debugXvfb = (0, logger_namespaceObject.getDebug)('computer:xvfb');
43
+ function checkXvfbInstalled() {
44
+ try {
45
+ (0, external_node_child_process_namespaceObject.execSync)('which Xvfb', {
46
+ stdio: 'ignore'
47
+ });
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function findAvailableDisplay(startFrom = 99) {
54
+ for(let n = startFrom; n < startFrom + 100; n++)if (!(0, external_node_fs_namespaceObject.existsSync)(`/tmp/.X${n}-lock`)) return n;
55
+ throw new Error(`No available display number found (checked ${startFrom} to ${startFrom + 99})`);
56
+ }
57
+ function startXvfb(options) {
58
+ const resolution = options?.resolution || '1920x1080x24';
59
+ const displayNum = options?.displayNumber ?? findAvailableDisplay();
60
+ const display = `:${displayNum}`;
61
+ return new Promise((resolve, reject)=>{
62
+ debugXvfb(`Starting Xvfb on display ${display} with resolution ${resolution}`);
63
+ const xvfbProcess = (0, external_node_child_process_namespaceObject.spawn)('Xvfb', [
64
+ display,
65
+ '-screen',
66
+ '0',
67
+ resolution,
68
+ '-ac',
69
+ '-nolisten',
70
+ 'tcp'
71
+ ], {
72
+ stdio: 'ignore'
73
+ });
74
+ let settled = false;
75
+ xvfbProcess.on('error', (err)=>{
76
+ if (!settled) {
77
+ settled = true;
78
+ reject(new Error(`Failed to start Xvfb: ${err.message}`));
79
+ }
80
+ });
81
+ xvfbProcess.on('exit', (code)=>{
82
+ if (!settled) {
83
+ settled = true;
84
+ reject(new Error(`Xvfb exited unexpectedly with code ${code}`));
85
+ }
86
+ });
87
+ const instance = {
88
+ process: xvfbProcess,
89
+ display,
90
+ stop () {
91
+ try {
92
+ xvfbProcess.kill('SIGTERM');
93
+ } catch {}
94
+ }
95
+ };
96
+ setTimeout(()=>{
97
+ if (!settled) {
98
+ settled = true;
99
+ debugXvfb(`Xvfb started on display ${display}`);
100
+ resolve(instance);
101
+ }
102
+ }, 500);
103
+ });
104
+ }
105
+ function needsXvfb(explicitOpt) {
106
+ if ('linux' !== process.platform) return false;
107
+ return true === explicitOpt;
108
+ }
41
109
  function _define_property(obj, key, value) {
42
110
  if (key in obj) Object.defineProperty(obj, key, {
43
111
  value: value,
@@ -208,53 +276,74 @@ class ComputerDevice {
208
276
  async connect() {
209
277
  debugDevice('Connecting to computer device');
210
278
  try {
279
+ const headless = this.options?.headless ?? 'true' === process.env.MIDSCENE_COMPUTER_HEADLESS_LINUX;
280
+ if (needsXvfb(headless)) {
281
+ if (!checkXvfbInstalled()) throw new Error('Xvfb is required for headless mode but not installed. Install: sudo apt-get install xvfb');
282
+ this.xvfbInstance = await startXvfb({
283
+ resolution: this.options?.xvfbResolution
284
+ });
285
+ process.env.DISPLAY = this.xvfbInstance.display;
286
+ debugDevice(`Xvfb started on display ${this.xvfbInstance.display}`);
287
+ this.xvfbCleanup = ()=>{
288
+ if (this.xvfbInstance) {
289
+ this.xvfbInstance.stop();
290
+ this.xvfbInstance = void 0;
291
+ }
292
+ };
293
+ process.on('exit', this.xvfbCleanup);
294
+ process.on('SIGINT', this.xvfbCleanup);
295
+ process.on('SIGTERM', this.xvfbCleanup);
296
+ }
211
297
  libnut = await getLibnut();
212
298
  const size = await this.size();
213
299
  const displays = await ComputerDevice.listDisplays();
300
+ const headlessInfo = this.xvfbInstance ? `\nHeadless: true (Xvfb on ${this.xvfbInstance.display})` : '';
214
301
  this.description = `
215
302
  Type: Computer
216
303
  Platform: ${process.platform}
217
304
  Display: ${this.displayId || 'Primary'}
218
305
  Screen Size: ${size.width}x${size.height}
219
- Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}
306
+ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}${headlessInfo}
220
307
  `;
221
308
  debugDevice('Computer device connected', this.description);
309
+ await this.healthCheck();
222
310
  } catch (error) {
311
+ if (this.xvfbInstance) {
312
+ this.xvfbInstance.stop();
313
+ this.xvfbInstance = void 0;
314
+ }
223
315
  debugDevice(`Failed to connect: ${error}`);
224
316
  throw new Error(`Unable to connect to computer device: ${error}`);
225
317
  }
226
- await this.healthCheck();
227
318
  }
228
319
  async healthCheck() {
229
320
  console.log('[HealthCheck] Starting health check...');
230
321
  console.log('[HealthCheck] Taking screenshot...');
231
- try {
232
- const base64 = await this.screenshotBase64();
233
- console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
234
- } catch (error) {
235
- console.error(`[HealthCheck] Screenshot failed: ${error}`);
236
- process.exit(1);
237
- }
322
+ const screenshotTimeout = 15000;
323
+ let timeoutId;
324
+ const timeoutPromise = new Promise((_, reject)=>{
325
+ timeoutId = setTimeout(()=>reject(new Error('Screenshot timed out')), screenshotTimeout);
326
+ });
327
+ const base64 = await Promise.race([
328
+ this.screenshotBase64().finally(()=>clearTimeout(timeoutId)),
329
+ timeoutPromise
330
+ ]);
331
+ console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
238
332
  console.log('[HealthCheck] Moving mouse...');
239
- try {
240
- external_node_assert_default()(libnut, 'libnut not initialized');
241
- const startPos = libnut.getMousePos();
242
- console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
243
- const offsetX = Math.floor(40 * Math.random()) + 10;
244
- const offsetY = Math.floor(40 * Math.random()) + 10;
245
- const targetX = startPos.x + offsetX;
246
- const targetY = startPos.y + offsetY;
247
- console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
248
- libnut.moveMouse(targetX, targetY);
249
- await (0, utils_namespaceObject.sleep)(50);
250
- const movedPos = libnut.getMousePos();
251
- console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
252
- libnut.moveMouse(startPos.x, startPos.y);
253
- console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
254
- } catch (error) {
255
- console.error(`[HealthCheck] Mouse move failed: ${error}`);
256
- process.exit(1);
257
- }
333
+ external_node_assert_default()(libnut, 'libnut not initialized');
334
+ const startPos = libnut.getMousePos();
335
+ console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
336
+ const offsetX = Math.floor(40 * Math.random()) + 10;
337
+ const offsetY = Math.floor(40 * Math.random()) + 10;
338
+ const targetX = startPos.x + offsetX;
339
+ const targetY = startPos.y + offsetY;
340
+ console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
341
+ libnut.moveMouse(targetX, targetY);
342
+ await (0, utils_namespaceObject.sleep)(50);
343
+ const movedPos = libnut.getMousePos();
344
+ console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
345
+ libnut.moveMouse(startPos.x, startPos.y);
346
+ console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
258
347
  console.log('[HealthCheck] Health check passed');
259
348
  }
260
349
  async screenshotBase64() {
@@ -284,8 +373,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
284
373
  const screenSize = libnut.getScreenSize();
285
374
  return {
286
375
  width: screenSize.width,
287
- height: screenSize.height,
288
- dpr: 1
376
+ height: screenSize.height
289
377
  };
290
378
  } catch (error) {
291
379
  debugDevice(`Failed to get screen size: ${error}`);
@@ -550,6 +638,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
550
638
  }
551
639
  async destroy() {
552
640
  if (this.destroyed) return;
641
+ if (this.xvfbInstance) {
642
+ this.xvfbInstance.stop();
643
+ this.xvfbInstance = void 0;
644
+ }
645
+ if (this.xvfbCleanup) {
646
+ process.removeListener('exit', this.xvfbCleanup);
647
+ process.removeListener('SIGINT', this.xvfbCleanup);
648
+ process.removeListener('SIGTERM', this.xvfbCleanup);
649
+ this.xvfbCleanup = void 0;
650
+ }
553
651
  this.destroyed = true;
554
652
  debugDevice('Computer device destroyed');
555
653
  }
@@ -562,6 +660,8 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
562
660
  _define_property(this, "displayId", void 0);
563
661
  _define_property(this, "description", void 0);
564
662
  _define_property(this, "destroyed", false);
663
+ _define_property(this, "xvfbInstance", void 0);
664
+ _define_property(this, "xvfbCleanup", void 0);
565
665
  _define_property(this, "useAppleScript", void 0);
566
666
  _define_property(this, "uri", void 0);
567
667
  this.options = options;
@@ -590,7 +690,7 @@ class ComputerMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
590
690
  createTemporaryDevice() {
591
691
  return new ComputerDevice({});
592
692
  }
593
- async ensureAgent(displayId) {
693
+ async ensureAgent(displayId, headless) {
594
694
  if (this.agent && displayId) {
595
695
  try {
596
696
  await this.agent.destroy?.();
@@ -601,10 +701,15 @@ class ComputerMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
601
701
  }
602
702
  if (this.agent) return this.agent;
603
703
  debug('Creating Computer agent with displayId:', displayId || 'primary');
604
- const opts = displayId ? {
605
- displayId
606
- } : void 0;
607
- const agent = await agentFromComputer(opts);
704
+ const opts = {
705
+ ...displayId ? {
706
+ displayId
707
+ } : {},
708
+ ...void 0 !== headless ? {
709
+ headless
710
+ } : {}
711
+ };
712
+ const agent = await agentFromComputer(Object.keys(opts).length > 0 ? opts : void 0);
608
713
  this.agent = agent;
609
714
  return agent;
610
715
  }
@@ -614,10 +719,11 @@ class ComputerMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
614
719
  name: 'computer_connect',
615
720
  description: 'Connect to computer desktop. Provide displayId to connect to a specific display (use computer_list_displays to get available IDs). If not provided, uses the primary display.',
616
721
  schema: {
617
- displayId: core_namespaceObject.z.string().optional().describe('Display ID (from computer_list_displays)')
722
+ displayId: core_namespaceObject.z.string().optional().describe('Display ID (from computer_list_displays)'),
723
+ headless: core_namespaceObject.z.boolean().optional().describe('Start virtual display via Xvfb (Linux only)')
618
724
  },
619
- handler: async ({ displayId })=>{
620
- const agent = await this.ensureAgent(displayId);
725
+ handler: async ({ displayId, headless })=>{
726
+ const agent = await this.ensureAgent(displayId, headless);
621
727
  const screenshot = await agent.interface.screenshotBase64();
622
728
  return {
623
729
  content: [