@debugelectron/debug-electron-mcp 1.6.9 → 1.6.10

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.
package/README.md CHANGED
@@ -48,7 +48,7 @@ Transform your Electron development experience with **AI-powered automation**:
48
48
  {
49
49
  "mcp": {
50
50
  "servers": {
51
- "electron": {
51
+ "debug-electron-mcp": {
52
52
  "command": "npx",
53
53
  "args": ["-y", "@debugelectron/debug-electron-mcp@latest"]
54
54
  }
@@ -61,7 +61,7 @@ Transform your Electron development experience with **AI-powered automation**:
61
61
  ```json
62
62
  {
63
63
  "mcpServers": {
64
- "electron": {
64
+ "debug-electron-mcp": {
65
65
  "command": "npx",
66
66
  "args": ["-y", "@debugelectron/debug-electron-mcp@latest"]
67
67
  }
@@ -73,7 +73,7 @@ Transform your Electron development experience with **AI-powered automation**:
73
73
  ```json
74
74
  {
75
75
  "mcpServers": {
76
- "electron": {
76
+ "debug-electron-mcp": {
77
77
  "command": "npx",
78
78
  "args": ["-y", "@debugelectron/debug-electron-mcp@latest"]
79
79
  }
@@ -316,7 +316,14 @@ When configuring an Electron app for MCP Server:
316
316
  | --------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------ |
317
317
  | `click_by_selector` | `{"selector": "css-selector"}` | `{"selector": "button.primary"}` |
318
318
  | `click_by_text` | `{"text": "button text"}` | `{"text": "Submit"}` |
319
+ | `hover` | `{"selector": "css-selector"}` | `{"selector": "#menu-item"}` |
320
+ | `drag` | `{"startSelector": "source", "endSelector": "target"}` | `{"startSelector": "#item", "endSelector": "#zone"}` |
321
+ | `wait` | `{"duration": 1000}` or `{"selector": "..."}` or `{"text": "..."}` | `{"selector": "#loaded-content"}` |
322
+ | `type` | `{"selector": "...", "text": "..."}` | `{"selector": "#search", "text": "hello"}` |
319
323
  | `fill_input` | `{"value": "text", "selector": "..."}` or `{"value": "text", "placeholder": "..."}` | `{"placeholder": "Enter name", "value": "John"}` |
324
+ | `get_attribute` | `{"selector": "...", "attribute": "..."}` | `{"selector": "img", "attribute": "src"}` |
325
+ | `is_visible` | `{"selector": "..."}` | `{"selector": "#error-message"}` |
326
+ | `count` | `{"selector": "..."}` | `{"selector": ".list-item"}` |
320
327
  | `send_keyboard_shortcut` | `{"text": "key combination"}` | `{"text": "Ctrl+N"}` |
321
328
  | `eval` | `{"code": "javascript"}` | `{"code": "document.title"}` |
322
329
  | `get_title`, `get_url`, `get_body_text` | No args needed | `{}` or omit args |
@@ -441,6 +448,13 @@ Execute JavaScript commands in the running Electron application via WebSocket.
441
448
  - `click_by_text`: Click elements by their visible text, aria-label, or title (more reliable than selectors)
442
449
  - `fill_input`: Fill input fields by selector, placeholder text, or associated label text
443
450
  - `select_option`: Select dropdown options by value or visible text
451
+ - `hover`: Hover over elements (simulates mouseenter/mouseover)
452
+ - `drag`: Drag elements from one location to another (supports dataTransfer)
453
+ - `wait`: Wait for duration, element selector, or text presence
454
+ - `type`: Type text character-by-character into inputs
455
+ - `get_attribute`: Get value of any element attribute
456
+ - `is_visible`: Check if element exists and is visible in viewport
457
+ - `count`: Count elements matching a selector (total and visible)
444
458
  - `get_page_structure`: Get organized overview of all page elements (buttons, inputs, selects, links)
445
459
  - `get_title`: Get document title
446
460
  - `get_url`: Get current URL
package/dist/index.js CHANGED
@@ -4289,7 +4289,14 @@ const TakeScreenshotSchema = external_zod_namespaceObject.z.object({
4289
4289
  .string()
4290
4290
  .optional()
4291
4291
  .describe('Path to save the screenshot (optional, defaults to temp directory)'),
4292
- windowTitle: external_zod_namespaceObject.z.string().optional().describe('Specific window title to screenshot (optional)'),
4292
+ targetId: external_zod_namespaceObject.z
4293
+ .string()
4294
+ .optional()
4295
+ .describe('CDP target ID to screenshot a specific window (exact match, takes priority over windowTitle)'),
4296
+ windowTitle: external_zod_namespaceObject.z
4297
+ .string()
4298
+ .optional()
4299
+ .describe('Window title to screenshot (case-insensitive partial match). Use list_electron_windows to see available windows.'),
4293
4300
  });
4294
4301
  const ReadElectronLogsSchema = external_zod_namespaceObject.z.object({
4295
4302
  logType: external_zod_namespaceObject.z
@@ -4321,17 +4328,26 @@ var ToolName;
4321
4328
  ToolName["GET_ELECTRON_WINDOW_INFO"] = "get_electron_window_info";
4322
4329
  ToolName["LIST_ELECTRON_WINDOWS"] = "list_electron_windows";
4323
4330
  })(ToolName || (ToolName = {}));
4331
+ // Helper function to convert Zod schemas to JSON Schema with proper typing
4332
+ // Using 'as any' to work around deep type instantiation issues with zod-to-json-schema
4333
+ const toJsonSchema = (schema) => (0,external_zod_to_json_schema_namespaceObject.zodToJsonSchema)(schema);
4324
4334
  // Define tools available to the MCP server
4325
4335
  const tools = [
4326
4336
  {
4327
4337
  name: ToolName.GET_ELECTRON_WINDOW_INFO,
4328
4338
  description: 'Get information about running Electron applications and their windows. Automatically detects any Electron app with remote debugging enabled (port 9222).',
4329
- inputSchema: (0,external_zod_to_json_schema_namespaceObject.zodToJsonSchema)(GetElectronWindowInfoSchema),
4339
+ inputSchema: toJsonSchema(GetElectronWindowInfoSchema),
4330
4340
  },
4331
4341
  {
4332
4342
  name: ToolName.TAKE_SCREENSHOT,
4333
- description: 'Take a screenshot of any running Electron application window. Returns base64 image data for AI analysis. No files created unless outputPath is specified.',
4334
- inputSchema: (0,external_zod_to_json_schema_namespaceObject.zodToJsonSchema)(TakeScreenshotSchema),
4343
+ description: `Take a screenshot of any running Electron application window. Returns base64 image data for AI analysis. No files created unless outputPath is specified.
4344
+
4345
+ Multi-window support:
4346
+ - targetId: Specify a CDP target ID to screenshot a specific window (exact match, takes priority)
4347
+ - windowTitle: Specify a window title to target (case-insensitive partial match)
4348
+ - If neither is specified, screenshots the first available main window (backward compatible)
4349
+ - Use 'list_electron_windows' to see available windows and their IDs`,
4350
+ inputSchema: toJsonSchema(TakeScreenshotSchema),
4335
4351
  },
4336
4352
  {
4337
4353
  name: ToolName.SEND_COMMAND_TO_ELECTRON,
@@ -4351,6 +4367,15 @@ Enhanced UI interaction commands:
4351
4367
  - 'get_title', 'get_url', 'get_body_text': Basic page information
4352
4368
  - 'eval': Execute custom JavaScript code with enhanced error reporting
4353
4369
 
4370
+ Additional interaction commands:
4371
+ - 'hover': Hover over an element by selector (triggers mouseenter/mouseover events)
4372
+ - 'drag': Drag from one element to another {"startSelector": "...", "endSelector": "..."}
4373
+ - 'wait': Wait for element, text, or duration {"selector": "..."} or {"text": "..."} or {"duration": 1000}
4374
+ - 'type': Type text character by character {"text": "...", "selector": "..."} (triggers key events)
4375
+ - 'get_attribute': Get element attribute value {"selector": "...", "attribute": "href"}
4376
+ - 'is_visible': Check if element is visible {"selector": "..."} returns visibility info
4377
+ - 'count': Count elements matching selector {"selector": "..."} returns total and visible count
4378
+
4354
4379
  IMPORTANT: Arguments must be passed as an object with the correct properties:
4355
4380
 
4356
4381
  Examples:
@@ -4359,6 +4384,13 @@ Examples:
4359
4384
  - fill_input: {"placeholder": "Enter name", "value": "John Doe"}
4360
4385
  - fill_input: {"selector": "#email", "value": "user@example.com"}
4361
4386
  - send_keyboard_shortcut: {"text": "Enter"}
4387
+ - hover: {"selector": ".dropdown-trigger"}
4388
+ - drag: {"startSelector": ".draggable", "endSelector": ".drop-zone"}
4389
+ - wait: {"selector": ".loading-spinner", "timeout": 5000}
4390
+ - type: {"text": "Hello World", "selector": "#input-field"}
4391
+ - get_attribute: {"selector": "a.link", "attribute": "href"}
4392
+ - is_visible: {"selector": ".modal"}
4393
+ - count: {"selector": "li.item"}
4362
4394
  - eval: {"code": "document.title"}
4363
4395
 
4364
4396
  Use 'get_page_structure' or 'debug_elements' first to understand available elements, then use specific interaction commands.
@@ -4368,17 +4400,17 @@ Multi-window support:
4368
4400
  - windowTitle: Specify a window title to target (case-insensitive partial match)
4369
4401
  - If neither is specified, commands are sent to the first available main window (backward compatible)
4370
4402
  - Use 'list_electron_windows' to see available windows and their IDs`,
4371
- inputSchema: (0,external_zod_to_json_schema_namespaceObject.zodToJsonSchema)(SendCommandToElectronSchema),
4403
+ inputSchema: toJsonSchema(SendCommandToElectronSchema),
4372
4404
  },
4373
4405
  {
4374
4406
  name: ToolName.LIST_ELECTRON_WINDOWS,
4375
4407
  description: "List all available Electron window targets across all detected applications. Returns window IDs, titles, URLs, and ports. Use the returned IDs with send_command_to_electron's targetId parameter to target specific windows.",
4376
- inputSchema: (0,external_zod_to_json_schema_namespaceObject.zodToJsonSchema)(ListElectronWindowsSchema),
4408
+ inputSchema: toJsonSchema(ListElectronWindowsSchema),
4377
4409
  },
4378
4410
  {
4379
4411
  name: ToolName.READ_ELECTRON_LOGS,
4380
4412
  description: 'Read console logs and output from running Electron applications. Useful for debugging and monitoring app behavior.',
4381
- inputSchema: (0,external_zod_to_json_schema_namespaceObject.zodToJsonSchema)(ReadElectronLogsSchema),
4413
+ inputSchema: toJsonSchema(ReadElectronLogsSchema),
4382
4414
  },
4383
4415
  ];
4384
4416
 
@@ -6277,10 +6309,379 @@ async function sendCommandToElectron(command, args, windowOptions) {
6277
6309
 
6278
6310
  return JSON.stringify({ forms, formCount: forms.length }, null, 2);
6279
6311
  })()
6312
+ `;
6313
+ break;
6314
+ case 'hover':
6315
+ // Hover over an element
6316
+ const hoverSelector = args?.selector || '';
6317
+ if (!hoverSelector) {
6318
+ return 'ERROR: Missing selector. Use: {"selector": "your-css-selector"}';
6319
+ }
6320
+ if (hoverSelector.includes('javascript:') || hoverSelector.includes('<script')) {
6321
+ return 'Invalid selector: contains dangerous content';
6322
+ }
6323
+ const escapedHoverSelector = JSON.stringify(hoverSelector);
6324
+ javascriptCode = `
6325
+ (function() {
6326
+ try {
6327
+ const element = document.querySelector(${escapedHoverSelector});
6328
+ if (!element) {
6329
+ return 'Element not found: ' + ${escapedHoverSelector};
6330
+ }
6331
+
6332
+ const rect = element.getBoundingClientRect();
6333
+ if (rect.width === 0 || rect.height === 0) {
6334
+ return 'Element not visible';
6335
+ }
6336
+
6337
+ const mouseenterEvent = new MouseEvent('mouseenter', {
6338
+ bubbles: false,
6339
+ cancelable: true,
6340
+ view: window,
6341
+ clientX: rect.left + rect.width / 2,
6342
+ clientY: rect.top + rect.height / 2
6343
+ });
6344
+
6345
+ const mouseoverEvent = new MouseEvent('mouseover', {
6346
+ bubbles: true,
6347
+ cancelable: true,
6348
+ view: window,
6349
+ clientX: rect.left + rect.width / 2,
6350
+ clientY: rect.top + rect.height / 2
6351
+ });
6352
+
6353
+ element.dispatchEvent(mouseenterEvent);
6354
+ element.dispatchEvent(mouseoverEvent);
6355
+
6356
+ return 'Hovered over element: ' + element.tagName +
6357
+ (element.textContent ? ' - "' + element.textContent.substring(0, 50).trim() + '"' : '');
6358
+ } catch (e) {
6359
+ return 'Error hovering: ' + e.message;
6360
+ }
6361
+ })();
6362
+ `;
6363
+ break;
6364
+ case 'drag':
6365
+ // Drag from one element to another
6366
+ const startSel = args?.startSelector || args?.selector || '';
6367
+ const endSel = args?.endSelector || '';
6368
+ if (!startSel || !endSel) {
6369
+ return 'ERROR: Missing selectors. Use: {"startSelector": "source-element", "endSelector": "target-element"}';
6370
+ }
6371
+ if (startSel.includes('javascript:') || endSel.includes('javascript:')) {
6372
+ return 'Invalid selector: contains dangerous content';
6373
+ }
6374
+ const escapedStartSel = JSON.stringify(startSel);
6375
+ const escapedEndSel = JSON.stringify(endSel);
6376
+ javascriptCode = `
6377
+ (function() {
6378
+ try {
6379
+ const startElement = document.querySelector(${escapedStartSel});
6380
+ const endElement = document.querySelector(${escapedEndSel});
6381
+
6382
+ if (!startElement) return 'Start element not found: ' + ${escapedStartSel};
6383
+ if (!endElement) return 'End element not found: ' + ${escapedEndSel};
6384
+
6385
+ const startRect = startElement.getBoundingClientRect();
6386
+ const endRect = endElement.getBoundingClientRect();
6387
+
6388
+ const startX = startRect.left + startRect.width / 2;
6389
+ const startY = startRect.top + startRect.height / 2;
6390
+ const endX = endRect.left + endRect.width / 2;
6391
+ const endY = endRect.top + endRect.height / 2;
6392
+
6393
+ const dataTransfer = new DataTransfer();
6394
+ dataTransfer.setData('text/plain', startElement.id || '');
6395
+ dataTransfer.effectAllowed = 'copyMove';
6396
+ dataTransfer.dropEffect = 'move';
6397
+
6398
+ const dragStartEvent = new DragEvent('dragstart', {
6399
+ bubbles: true, cancelable: true, clientX: startX, clientY: startY, dataTransfer: dataTransfer
6400
+ });
6401
+ const dragEvent = new DragEvent('drag', {
6402
+ bubbles: true, cancelable: true, clientX: endX, clientY: endY, dataTransfer: dataTransfer
6403
+ });
6404
+ const dragEnterEvent = new DragEvent('dragenter', {
6405
+ bubbles: true, cancelable: true, clientX: endX, clientY: endY, dataTransfer: dataTransfer
6406
+ });
6407
+ const dragOverEvent = new DragEvent('dragover', {
6408
+ bubbles: true, cancelable: true, clientX: endX, clientY: endY, dataTransfer: dataTransfer
6409
+ });
6410
+ const dropEvent = new DragEvent('drop', {
6411
+ bubbles: true, cancelable: true, clientX: endX, clientY: endY, dataTransfer: dataTransfer
6412
+ });
6413
+ const dragEndEvent = new DragEvent('dragend', {
6414
+ bubbles: true, cancelable: true, clientX: endX, clientY: endY, dataTransfer: dataTransfer
6415
+ });
6416
+
6417
+ startElement.dispatchEvent(dragStartEvent);
6418
+ startElement.dispatchEvent(dragEvent);
6419
+ endElement.dispatchEvent(dragEnterEvent);
6420
+ endElement.dispatchEvent(dragOverEvent);
6421
+ endElement.dispatchEvent(dropEvent);
6422
+ startElement.dispatchEvent(dragEndEvent);
6423
+
6424
+ return 'Dragged from ' + startElement.tagName + ' to ' + endElement.tagName;
6425
+ } catch (e) {
6426
+ return 'Error performing drag: ' + e.message;
6427
+ }
6428
+ })();
6429
+ `;
6430
+ break;
6431
+ case 'wait':
6432
+ // Wait for element, text, or specified time
6433
+ const waitSelector = args?.selector || '';
6434
+ const waitText = args?.text || '';
6435
+ const waitDuration = args?.duration || 0;
6436
+ const waitTimeout = args?.timeout || 5000;
6437
+ if (!waitSelector && !waitText && !waitDuration) {
6438
+ return 'ERROR: Specify selector, text, or duration. Use: {"selector": "..."} or {"text": "..."} or {"duration": 1000}';
6439
+ }
6440
+ if (waitDuration > 0) {
6441
+ javascriptCode = `
6442
+ (function() {
6443
+ return new Promise(resolve => {
6444
+ setTimeout(() => resolve('Waited ${waitDuration}ms'), ${waitDuration});
6445
+ });
6446
+ })();
6447
+ `;
6448
+ }
6449
+ else if (waitSelector) {
6450
+ const escapedWaitSelector = JSON.stringify(waitSelector);
6451
+ javascriptCode = `
6452
+ (function() {
6453
+ return new Promise((resolve) => {
6454
+ const startTime = Date.now();
6455
+ const timeout = ${waitTimeout};
6456
+
6457
+ function check() {
6458
+ const element = document.querySelector(${escapedWaitSelector});
6459
+ if (element && element.getBoundingClientRect().width > 0) {
6460
+ resolve('Element found: ' + ${escapedWaitSelector} + ' (after ' + (Date.now() - startTime) + 'ms)');
6461
+ return;
6462
+ }
6463
+ if (Date.now() - startTime > timeout) {
6464
+ resolve('Timeout waiting for element: ' + ${escapedWaitSelector});
6465
+ return;
6466
+ }
6467
+ setTimeout(check, 100);
6468
+ }
6469
+ check();
6470
+ });
6471
+ })();
6472
+ `;
6473
+ }
6474
+ else if (waitText) {
6475
+ const escapedWaitText = JSON.stringify(waitText);
6476
+ javascriptCode = `
6477
+ (function() {
6478
+ return new Promise((resolve) => {
6479
+ const startTime = Date.now();
6480
+ const timeout = ${waitTimeout};
6481
+
6482
+ function check() {
6483
+ if (document.body.innerText.includes(${escapedWaitText})) {
6484
+ resolve('Text found: ' + ${escapedWaitText} + ' (after ' + (Date.now() - startTime) + 'ms)');
6485
+ return;
6486
+ }
6487
+ if (Date.now() - startTime > timeout) {
6488
+ resolve('Timeout waiting for text: ' + ${escapedWaitText});
6489
+ return;
6490
+ }
6491
+ setTimeout(check, 100);
6492
+ }
6493
+ check();
6494
+ });
6495
+ })();
6496
+ `;
6497
+ }
6498
+ else {
6499
+ javascriptCode = `'Invalid wait parameters'`;
6500
+ }
6501
+ break;
6502
+ case 'type':
6503
+ // Type text character by character (different from fill which sets value directly)
6504
+ const typeText = args?.text || '';
6505
+ const typeSelector = args?.selector || '';
6506
+ const typeSlowly = args?.slowly !== false; // Default to true for realistic typing
6507
+ if (!typeText) {
6508
+ return 'ERROR: Missing text. Use: {"text": "text to type"} or {"text": "...", "selector": "input-selector"}';
6509
+ }
6510
+ const escapedTypeText = JSON.stringify(typeText);
6511
+ const escapedTypeSelector = typeSelector ? JSON.stringify(typeSelector) : 'null';
6512
+ javascriptCode = `
6513
+ (function() {
6514
+ try {
6515
+ let element;
6516
+ if (${escapedTypeSelector}) {
6517
+ element = document.querySelector(${escapedTypeSelector});
6518
+ if (!element) return 'Element not found: ' + ${escapedTypeSelector};
6519
+ } else {
6520
+ element = document.activeElement;
6521
+ if (!element || element === document.body) {
6522
+ return 'No element focused. Use {"selector": "..."} to specify an input';
6523
+ }
6524
+ }
6525
+
6526
+ element.focus();
6527
+
6528
+ const text = ${escapedTypeText};
6529
+ const slowly = ${typeSlowly};
6530
+
6531
+ if (slowly) {
6532
+ return new Promise((resolve) => {
6533
+ let index = 0;
6534
+
6535
+ function typeNext() {
6536
+ if (index >= text.length) {
6537
+ resolve('Typed: "' + text + '" into ' + element.tagName);
6538
+ return;
6539
+ }
6540
+
6541
+ const char = text[index];
6542
+
6543
+ element.dispatchEvent(new KeyboardEvent('keydown', {
6544
+ key: char,
6545
+ code: 'Key' + char.toUpperCase(),
6546
+ bubbles: true
6547
+ }));
6548
+
6549
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
6550
+ element.value += char;
6551
+ element.dispatchEvent(new Event('input', { bubbles: true }));
6552
+ }
6553
+
6554
+ element.dispatchEvent(new KeyboardEvent('keyup', {
6555
+ key: char,
6556
+ code: 'Key' + char.toUpperCase(),
6557
+ bubbles: true
6558
+ }));
6559
+
6560
+ index++;
6561
+ setTimeout(typeNext, 50);
6562
+ }
6563
+
6564
+ typeNext();
6565
+ });
6566
+ } else {
6567
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
6568
+ element.value += text;
6569
+ element.dispatchEvent(new Event('input', { bubbles: true }));
6570
+ element.dispatchEvent(new Event('change', { bubbles: true }));
6571
+ }
6572
+ return 'Typed: "' + text + '" into ' + element.tagName;
6573
+ }
6574
+ } catch (e) {
6575
+ return 'Error typing: ' + e.message;
6576
+ }
6577
+ })();
6578
+ `;
6579
+ break;
6580
+ case 'get_attribute':
6581
+ // Get an attribute value from an element
6582
+ const attrSelector = args?.selector || '';
6583
+ const attrName = args?.attribute || '';
6584
+ if (!attrSelector || !attrName) {
6585
+ return 'ERROR: Missing selector or attribute. Use: {"selector": "...", "attribute": "href"}';
6586
+ }
6587
+ const escapedAttrSelector = JSON.stringify(attrSelector);
6588
+ const escapedAttrName = JSON.stringify(attrName);
6589
+ javascriptCode = `
6590
+ (function() {
6591
+ try {
6592
+ const element = document.querySelector(${escapedAttrSelector});
6593
+ if (!element) {
6594
+ return 'Element not found: ' + ${escapedAttrSelector};
6595
+ }
6596
+
6597
+ const value = element.getAttribute(${escapedAttrName});
6598
+ if (value === null) {
6599
+ return 'Attribute not found: ' + ${escapedAttrName} + ' on ' + element.tagName;
6600
+ }
6601
+
6602
+ return JSON.stringify({
6603
+ selector: ${escapedAttrSelector},
6604
+ attribute: ${escapedAttrName},
6605
+ value: value
6606
+ });
6607
+ } catch (e) {
6608
+ return 'Error getting attribute: ' + e.message;
6609
+ }
6610
+ })();
6611
+ `;
6612
+ break;
6613
+ case 'is_visible':
6614
+ // Check if an element is visible
6615
+ const visSelector = args?.selector || '';
6616
+ if (!visSelector) {
6617
+ return 'ERROR: Missing selector. Use: {"selector": "your-css-selector"}';
6618
+ }
6619
+ const escapedVisSelector = JSON.stringify(visSelector);
6620
+ javascriptCode = `
6621
+ (function() {
6622
+ try {
6623
+ const element = document.querySelector(${escapedVisSelector});
6624
+ if (!element) {
6625
+ return JSON.stringify({ selector: ${escapedVisSelector}, exists: false, visible: false });
6626
+ }
6627
+
6628
+ const rect = element.getBoundingClientRect();
6629
+ const style = window.getComputedStyle(element);
6630
+
6631
+ const isVisible = rect.width > 0 &&
6632
+ rect.height > 0 &&
6633
+ style.visibility !== 'hidden' &&
6634
+ style.display !== 'none' &&
6635
+ style.opacity !== '0';
6636
+
6637
+ const inViewport = rect.top < window.innerHeight &&
6638
+ rect.bottom > 0 &&
6639
+ rect.left < window.innerWidth &&
6640
+ rect.right > 0;
6641
+
6642
+ return JSON.stringify({
6643
+ selector: ${escapedVisSelector},
6644
+ exists: true,
6645
+ visible: isVisible,
6646
+ inViewport: inViewport,
6647
+ dimensions: { width: rect.width, height: rect.height },
6648
+ position: { top: rect.top, left: rect.left }
6649
+ });
6650
+ } catch (e) {
6651
+ return 'Error checking visibility: ' + e.message;
6652
+ }
6653
+ })();
6654
+ `;
6655
+ break;
6656
+ case 'count':
6657
+ // Count elements matching a selector
6658
+ const countSelector = args?.selector || '';
6659
+ if (!countSelector) {
6660
+ return 'ERROR: Missing selector. Use: {"selector": "your-css-selector"}';
6661
+ }
6662
+ const escapedCountSelector = JSON.stringify(countSelector);
6663
+ javascriptCode = `
6664
+ (function() {
6665
+ try {
6666
+ const elements = document.querySelectorAll(${escapedCountSelector});
6667
+ const visibleCount = Array.from(elements).filter(el => {
6668
+ const rect = el.getBoundingClientRect();
6669
+ return rect.width > 0 && rect.height > 0;
6670
+ }).length;
6671
+
6672
+ return JSON.stringify({
6673
+ selector: ${escapedCountSelector},
6674
+ total: elements.length,
6675
+ visible: visibleCount
6676
+ });
6677
+ } catch (e) {
6678
+ return 'Error counting elements: ' + e.message;
6679
+ }
6680
+ })();
6280
6681
  `;
6281
6682
  break;
6282
6683
  case 'console_log':
6283
- javascriptCode = `console.log('MCP Command:', '${args?.message || 'Hello from MCP!'}'); 'Console message sent'`;
6684
+ javascriptCode = `console.log('MCP Command:', ${JSON.stringify(args?.message ?? 'Hello from MCP!')}); 'Console message sent'`;
6284
6685
  break;
6285
6686
  case 'eval':
6286
6687
  const rawCode = typeof args === 'string' ? args : args?.code || command;
@@ -6567,9 +6968,11 @@ const promises_namespaceObject = require("fs/promises");
6567
6968
  /**
6568
6969
  * Take a screenshot of the running Electron application using Chrome DevTools Protocol
6569
6970
  */
6570
- async function takeScreenshot(outputPath, windowTitle) {
6971
+ async function takeScreenshot(options = {}) {
6972
+ const { outputPath, targetId, windowTitle } = options;
6571
6973
  logger.info('📸 Taking screenshot of Electron application', {
6572
6974
  outputPath,
6975
+ targetId,
6573
6976
  windowTitle,
6574
6977
  timestamp: new Date().toISOString(),
6575
6978
  });
@@ -6579,12 +6982,39 @@ async function takeScreenshot(outputPath, windowTitle) {
6579
6982
  if (apps.length === 0) {
6580
6983
  throw new Error('No running Electron applications found with remote debugging enabled');
6581
6984
  }
6582
- // Use the first app found (or find by title if specified)
6985
+ // Find target app and target based on options
6986
+ // Priority: targetId > windowTitle > first available window
6583
6987
  let targetApp = apps[0];
6584
- if (windowTitle) {
6585
- const namedApp = apps.find((app) => app.targets.some((target) => target.title?.toLowerCase().includes(windowTitle.toLowerCase())));
6586
- if (namedApp) {
6587
- targetApp = namedApp;
6988
+ let targetInfo = null;
6989
+ if (targetId) {
6990
+ // Search for exact targetId match across all apps
6991
+ for (const app of apps) {
6992
+ const match = app.targets.find((t) => t.id === targetId);
6993
+ if (match) {
6994
+ targetApp = app;
6995
+ targetInfo = { port: app.port, targetId: match.id };
6996
+ logger.debug(`Found target by ID "${targetId}" on port ${app.port}`);
6997
+ break;
6998
+ }
6999
+ }
7000
+ if (!targetInfo) {
7001
+ throw new Error(`No window found with targetId "${targetId}". Use list_electron_windows to see available targets.`);
7002
+ }
7003
+ }
7004
+ else if (windowTitle) {
7005
+ // Search for case-insensitive partial title match
7006
+ const searchTitle = windowTitle.toLowerCase();
7007
+ for (const app of apps) {
7008
+ const match = app.targets.find((t) => t.title && t.title.toLowerCase().includes(searchTitle));
7009
+ if (match) {
7010
+ targetApp = app;
7011
+ targetInfo = { port: app.port, targetId: match.id };
7012
+ logger.debug(`Found target by title "${windowTitle}" on port ${app.port}`);
7013
+ break;
7014
+ }
7015
+ }
7016
+ if (!targetInfo) {
7017
+ throw new Error(`No window found with title matching "${windowTitle}". Use list_electron_windows to see available targets.`);
6588
7018
  }
6589
7019
  }
6590
7020
  // Connect to the Electron app's debugging port
@@ -6598,22 +7028,33 @@ async function takeScreenshot(outputPath, windowTitle) {
6598
7028
  if (pages.length === 0) {
6599
7029
  throw new Error('No pages found in the browser context');
6600
7030
  }
6601
- // Find the main application page (skip DevTools pages)
7031
+ // Find the target page
6602
7032
  let targetPage = pages[0];
6603
- for (const page of pages) {
6604
- const url = page.url();
6605
- const title = await page.title().catch(() => '');
6606
- // Skip DevTools and about:blank pages
6607
- if (!url.includes('devtools://') &&
6608
- !url.includes('about:blank') &&
6609
- title &&
6610
- !title.includes('DevTools')) {
6611
- // If windowTitle is specified, try to match it
6612
- if (windowTitle && title.toLowerCase().includes(windowTitle.toLowerCase())) {
6613
- targetPage = page;
6614
- break;
7033
+ if (targetInfo?.targetId) {
7034
+ // If we found a specific target, try to find the matching page
7035
+ // Note: We need to find the page by URL/title since CDP targetId doesn't directly map to Playwright pages
7036
+ const targetData = targetApp.targets.find((t) => t.id === targetInfo.targetId);
7037
+ if (targetData) {
7038
+ for (const page of pages) {
7039
+ const pageUrl = page.url();
7040
+ // Match by URL since that's more reliable than title
7041
+ if (pageUrl === targetData.url) {
7042
+ targetPage = page;
7043
+ break;
7044
+ }
6615
7045
  }
6616
- else if (!windowTitle) {
7046
+ }
7047
+ }
7048
+ else {
7049
+ // Find the main application page (skip DevTools pages)
7050
+ for (const page of pages) {
7051
+ const url = page.url();
7052
+ const title = await page.title().catch(() => '');
7053
+ // Skip DevTools and about:blank pages
7054
+ if (!url.includes('devtools://') &&
7055
+ !url.includes('about:blank') &&
7056
+ title &&
7057
+ !title.includes('DevTools')) {
6617
7058
  targetPage = page;
6618
7059
  break;
6619
7060
  }
@@ -6677,8 +7118,8 @@ async function handleToolCall(request) {
6677
7118
  };
6678
7119
  }
6679
7120
  case ToolName.TAKE_SCREENSHOT: {
6680
- const { outputPath, windowTitle } = TakeScreenshotSchema.parse(args);
6681
- const result = await takeScreenshot(outputPath, windowTitle);
7121
+ const { outputPath, targetId, windowTitle } = TakeScreenshotSchema.parse(args);
7122
+ const result = await takeScreenshot({ outputPath, targetId, windowTitle });
6682
7123
  const content = [];
6683
7124
  if (result.filePath) {
6684
7125
  content.push({