mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
Support multi window scenarios in vscode mcp server (#289250)
* Add window management tools for multi-window support * Refactor window management tools to remove title references and simplify responses * Refactor PlaywrightDriver to use readonly page property and remove _currentPage references * Update test/mcp/src/automationTools/windows.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix URL matching and CDP session handling in multi-window support (#289261) * Initial plan * Address code review feedback: improve URL matching and CDP session handling - Improve URL matching in switchToWindow to prefer exact matches before substring matches - Clear CDP session when switching windows to prevent using stale sessions - Add comprehensive documentation explaining the matching strategy and CDP session behavior Co-authored-by: rebornix <876920+rebornix@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rebornix <876920+rebornix@users.noreply.github.com> * Add automation tools for window interactions and update multiplex server configuration --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: rebornix <876920+rebornix@users.noreply.github.com>
This commit is contained in:
@@ -66,7 +66,7 @@ export class PlaywrightDriver {
|
||||
constructor(
|
||||
private readonly application: playwright.Browser | playwright.ElectronApplication,
|
||||
private readonly context: playwright.BrowserContext,
|
||||
private readonly page: playwright.Page,
|
||||
private _currentPage: playwright.Page,
|
||||
private readonly serverProcess: ChildProcess | undefined,
|
||||
private readonly whenLoaded: Promise<unknown>,
|
||||
private readonly options: LaunchOptions
|
||||
@@ -77,8 +77,284 @@ export class PlaywrightDriver {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
private get page(): playwright.Page {
|
||||
return this._currentPage;
|
||||
}
|
||||
|
||||
get currentPage(): playwright.Page {
|
||||
return this.page;
|
||||
return this._currentPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all open windows/pages.
|
||||
* For Electron apps, returns all Electron windows.
|
||||
* For browser contexts, returns all pages.
|
||||
*/
|
||||
getAllWindows(): playwright.Page[] {
|
||||
if ('windows' in this.application) {
|
||||
return (this.application as playwright.ElectronApplication).windows();
|
||||
}
|
||||
return this.context.pages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different window by index or URL pattern.
|
||||
* @param indexOrUrl - Window index (0-based) or a string to match against the URL.
|
||||
* When using a string, it first tries to find an exact URL match,
|
||||
* then falls back to finding the first URL that contains the pattern.
|
||||
* @returns The switched-to page, or undefined if not found
|
||||
* @note When switching windows, any existing CDP session will be cleared since it
|
||||
* remains attached to the previous page and cannot be used with the new page.
|
||||
*/
|
||||
switchToWindow(indexOrUrl: number | string): playwright.Page | undefined {
|
||||
const windows = this.getAllWindows();
|
||||
if (typeof indexOrUrl === 'number') {
|
||||
if (indexOrUrl >= 0 && indexOrUrl < windows.length) {
|
||||
this._currentPage = windows[indexOrUrl];
|
||||
// Clear CDP session as it's attached to the previous page
|
||||
this._cdpSession = undefined;
|
||||
return this._currentPage;
|
||||
}
|
||||
} else {
|
||||
// First try exact match, then fall back to substring match
|
||||
let found = windows.find(w => w.url() === indexOrUrl);
|
||||
if (!found) {
|
||||
found = windows.find(w => w.url().includes(indexOrUrl));
|
||||
}
|
||||
if (found) {
|
||||
this._currentPage = found;
|
||||
// Clear CDP session as it's attached to the previous page
|
||||
this._cdpSession = undefined;
|
||||
return this._currentPage;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about all windows.
|
||||
*/
|
||||
getWindowsInfo(): { index: number; url: string; isCurrent: boolean }[] {
|
||||
const windows = this.getAllWindows();
|
||||
return windows.map((p, index) => ({
|
||||
index,
|
||||
url: p.url(),
|
||||
isCurrent: p === this._currentPage
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot of the current window.
|
||||
* @param fullPage - Whether to capture the full scrollable page
|
||||
* @returns Screenshot as a Buffer
|
||||
*/
|
||||
async screenshotBuffer(fullPage: boolean = false): Promise<Buffer> {
|
||||
return await this.page.screenshot({
|
||||
type: 'png',
|
||||
fullPage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the accessibility snapshot of the current window.
|
||||
*/
|
||||
async getAccessibilitySnapshot(): Promise<playwright.Accessibility['snapshot'] extends () => Promise<infer T> ? T : never> {
|
||||
return await this.page.accessibility.snapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on an element using CSS selector with options.
|
||||
*/
|
||||
async clickSelector(selector: string, options?: { button?: 'left' | 'right' | 'middle'; clickCount?: number }): Promise<void> {
|
||||
await this.page.click(selector, {
|
||||
button: options?.button ?? 'left',
|
||||
clickCount: options?.clickCount ?? 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Type text into an element.
|
||||
* @param selector - CSS selector for the element
|
||||
* @param text - Text to type
|
||||
* @param slowly - Whether to type character by character (triggers key events)
|
||||
*/
|
||||
async typeText(selector: string, text: string, slowly: boolean = false): Promise<void> {
|
||||
if (slowly) {
|
||||
await this.page.type(selector, text, { delay: 50 });
|
||||
} else {
|
||||
await this.page.fill(selector, text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a JavaScript expression in the current window.
|
||||
*/
|
||||
async evaluateExpression<T = unknown>(expression: string): Promise<T> {
|
||||
return await this.page.evaluate(expression) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about elements matching a selector.
|
||||
*/
|
||||
async getLocatorInfo(selector: string, action?: 'count' | 'textContent' | 'innerHTML' | 'boundingBox' | 'isVisible'): Promise<
|
||||
number | string[] | { x: number; y: number; width: number; height: number } | null | boolean | { count: number; firstVisible: boolean }
|
||||
> {
|
||||
const locator = this.page.locator(selector);
|
||||
|
||||
switch (action) {
|
||||
case 'count':
|
||||
return await locator.count();
|
||||
case 'textContent':
|
||||
return await locator.allTextContents();
|
||||
case 'innerHTML':
|
||||
return await locator.allInnerTexts();
|
||||
case 'boundingBox':
|
||||
return await locator.first().boundingBox();
|
||||
case 'isVisible':
|
||||
return await locator.first().isVisible();
|
||||
default:
|
||||
return {
|
||||
count: await locator.count(),
|
||||
firstVisible: await locator.first().isVisible().catch(() => false)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an element to reach a specific state.
|
||||
*/
|
||||
async waitForElement(selector: string, options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden'; timeout?: number }): Promise<void> {
|
||||
await this.page.waitForSelector(selector, {
|
||||
state: options?.state ?? 'visible',
|
||||
timeout: options?.timeout ?? 30000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over an element.
|
||||
*/
|
||||
async hoverSelector(selector: string): Promise<void> {
|
||||
await this.page.hover(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag from one element to another.
|
||||
*/
|
||||
async dragSelector(sourceSelector: string, targetSelector: string): Promise<void> {
|
||||
await this.page.dragAndDrop(sourceSelector, targetSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Press a key or key combination.
|
||||
*/
|
||||
async pressKey(key: string): Promise<void> {
|
||||
await this.page.keyboard.press(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move mouse to a specific position.
|
||||
*/
|
||||
async mouseMove(x: number, y: number): Promise<void> {
|
||||
await this.page.mouse.move(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click at a specific position.
|
||||
*/
|
||||
async mouseClick(x: number, y: number, options?: { button?: 'left' | 'right' | 'middle'; clickCount?: number }): Promise<void> {
|
||||
await this.page.mouse.click(x, y, {
|
||||
button: options?.button ?? 'left',
|
||||
clickCount: options?.clickCount ?? 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag from one position to another.
|
||||
*/
|
||||
async mouseDrag(startX: number, startY: number, endX: number, endY: number): Promise<void> {
|
||||
await this.page.mouse.move(startX, startY);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(endX, endY);
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an option in a dropdown.
|
||||
*/
|
||||
async selectOption(selector: string, value: string | string[]): Promise<string[]> {
|
||||
return await this.page.selectOption(selector, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill multiple form fields at once.
|
||||
*/
|
||||
async fillForm(fields: { selector: string; value: string }[]): Promise<void> {
|
||||
for (const field of fields) {
|
||||
await this.page.fill(field.selector, field.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get console messages from the current window.
|
||||
*/
|
||||
async getConsoleMessages(): Promise<{ type: string; text: string }[]> {
|
||||
const messages = await this.page.consoleMessages();
|
||||
return messages.map(m => ({
|
||||
type: m.type(),
|
||||
text: m.text()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for text to appear, disappear, or a specified time to pass.
|
||||
*/
|
||||
async waitForText(options: { text?: string; textGone?: string; timeout?: number }): Promise<void> {
|
||||
const { text, textGone, timeout = 30000 } = options;
|
||||
|
||||
if (text) {
|
||||
await this.page.getByText(text).first().waitFor({ state: 'visible', timeout });
|
||||
}
|
||||
if (textGone) {
|
||||
await this.page.getByText(textGone).first().waitFor({ state: 'hidden', timeout });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specified time in milliseconds.
|
||||
*/
|
||||
async waitForTime(ms: number): Promise<void> {
|
||||
await new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an element is visible.
|
||||
*/
|
||||
async verifyElementVisible(selector: string): Promise<boolean> {
|
||||
try {
|
||||
await this.page.locator(selector).first().waitFor({ state: 'visible', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify text is visible on the page.
|
||||
*/
|
||||
async verifyTextVisible(text: string): Promise<boolean> {
|
||||
try {
|
||||
await this.page.getByText(text).first().waitFor({ state: 'visible', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of an input element.
|
||||
*/
|
||||
async getInputValue(selector: string): Promise<string> {
|
||||
return await this.page.inputValue(selector);
|
||||
}
|
||||
|
||||
async startTracing(name?: string): Promise<void> {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { applyLocalizationTools } from './localization.js';
|
||||
import { applyTaskTools } from './task.js';
|
||||
import { applyProfilerTools } from './profiler.js';
|
||||
import { applyChatTools } from './chat.js';
|
||||
import { applyWindowTools } from './windows.js';
|
||||
import { ApplicationService } from '../application';
|
||||
|
||||
/**
|
||||
@@ -93,6 +94,9 @@ export function applyAllTools(server: McpServer, appService: ApplicationService)
|
||||
// Chat Tools
|
||||
tools = tools.concat(applyChatTools(server, appService));
|
||||
|
||||
// Window Management Tools (for multi-window support)
|
||||
tools = tools.concat(applyWindowTools(server, appService));
|
||||
|
||||
// Return all registered tools
|
||||
return tools;
|
||||
}
|
||||
@@ -117,5 +121,6 @@ export {
|
||||
applyLocalizationTools,
|
||||
applyTaskTools,
|
||||
applyProfilerTools,
|
||||
applyChatTools
|
||||
applyChatTools,
|
||||
applyWindowTools
|
||||
};
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { ApplicationService } from '../application';
|
||||
|
||||
/**
|
||||
* Create a standardized text response for window tools
|
||||
*/
|
||||
function textResponse(text: string) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text }]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Management Tools for multi-window support.
|
||||
* These tools are thin wrappers around PlaywrightDriver methods.
|
||||
*/
|
||||
export function applyWindowTools(server: McpServer, appService: ApplicationService): RegisteredTool[] {
|
||||
const tools: RegisteredTool[] = [];
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_list_windows',
|
||||
'List all open VS Code windows with their index and URL',
|
||||
async () => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const windowInfo = app.code.driver.getWindowsInfo();
|
||||
return textResponse(`Open windows (${windowInfo.length}):\n${JSON.stringify(windowInfo, null, 2)}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_switch_window',
|
||||
'Switch to a different VS Code window by index or URL pattern (e.g., "agent.html")',
|
||||
{
|
||||
indexOrUrl: z.union([z.number(), z.string()]).describe('Window index (0-based) or URL pattern to match (e.g., "agent.html", "workbench")')
|
||||
},
|
||||
async ({ indexOrUrl }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const switched = app.code.driver.switchToWindow(indexOrUrl);
|
||||
|
||||
if (switched) {
|
||||
return textResponse(`Switched to window (URL: ${switched.url()})`);
|
||||
}
|
||||
|
||||
const windowInfo = app.code.driver.getWindowsInfo();
|
||||
const availableWindows = windowInfo.map(w => ` ${w.index}: ${w.url}`).join('\n');
|
||||
return textResponse(`Failed to switch window. Window not found for: ${indexOrUrl}\n\nAvailable windows:\n${availableWindows}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_get_current_window',
|
||||
'Get information about the currently active window',
|
||||
async () => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const windowInfo = driver.getWindowsInfo();
|
||||
const current = windowInfo.find(w => w.isCurrent);
|
||||
return textResponse(`Current window:\nIndex: ${current?.index ?? -1}\nURL: ${current?.url ?? 'Unknown'}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_screenshot',
|
||||
'Take a screenshot of the current window (respects the window set by vscode_automation_switch_window)',
|
||||
{
|
||||
fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page')
|
||||
},
|
||||
async ({ fullPage }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const screenshotBuffer = await driver.screenshotBuffer(fullPage ?? false);
|
||||
const url = driver.currentPage.url();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text' as const, text: `Screenshot (URL: ${url})` },
|
||||
{ type: 'image' as const, data: screenshotBuffer.toString('base64'), mimeType: 'image/png' }
|
||||
]
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_snapshot',
|
||||
'Capture accessibility snapshot of the current window. Returns the page structure with element references that can be used for interactions.',
|
||||
async () => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const snapshot = await driver.getAccessibilitySnapshot();
|
||||
const url = driver.currentPage.url();
|
||||
|
||||
return textResponse(`Page snapshot (URL: ${url}):\n\n${JSON.stringify(snapshot, null, 2)}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_click',
|
||||
'Click on an element in the current window using a CSS selector',
|
||||
{
|
||||
selector: z.string().describe('CSS selector for the element to click'),
|
||||
button: z.enum(['left', 'right', 'middle']).optional().describe('Mouse button to click'),
|
||||
clickCount: z.number().optional().describe('Number of clicks (1 for single, 2 for double)')
|
||||
},
|
||||
async ({ selector, button, clickCount }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.clickSelector(selector, { button, clickCount });
|
||||
return textResponse(`Clicked "${selector}"`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_type',
|
||||
'Type text into an element in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector for the element to type into'),
|
||||
text: z.string().describe('Text to type'),
|
||||
slowly: z.boolean().optional().describe('Whether to type one character at a time (useful for triggering key handlers)')
|
||||
},
|
||||
async ({ selector, text, slowly }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.typeText(selector, text, slowly ?? false);
|
||||
return textResponse(`Typed "${text}" into "${selector}"`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_evaluate',
|
||||
'Evaluate JavaScript in the current window',
|
||||
{
|
||||
expression: z.string().describe('JavaScript expression to evaluate')
|
||||
},
|
||||
async ({ expression }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const result = await driver.evaluateExpression(expression);
|
||||
return textResponse(`Result:\n${JSON.stringify(result, null, 2)}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_locator',
|
||||
'Get information about elements matching a selector in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector to find elements'),
|
||||
action: z.enum(['count', 'textContent', 'innerHTML', 'boundingBox', 'isVisible']).optional().describe('Action to perform on matched elements')
|
||||
},
|
||||
async ({ selector, action }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const result = await driver.getLocatorInfo(selector, action);
|
||||
return textResponse(`Locator "${selector}":\n${JSON.stringify(result, null, 2)}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_wait_for_selector',
|
||||
'Wait for an element to appear in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector to wait for'),
|
||||
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional().describe('State to wait for'),
|
||||
timeout: z.number().optional().describe('Timeout in milliseconds')
|
||||
},
|
||||
async ({ selector, state, timeout }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.waitForElement(selector, { state, timeout });
|
||||
return textResponse(`Element "${selector}" is now ${state ?? 'visible'}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_hover',
|
||||
'Hover over an element in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector for the element to hover over')
|
||||
},
|
||||
async ({ selector }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.hoverSelector(selector);
|
||||
return textResponse(`Hovered over "${selector}"`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_drag',
|
||||
'Drag from one element to another in the current window',
|
||||
{
|
||||
sourceSelector: z.string().describe('CSS selector for the source element'),
|
||||
targetSelector: z.string().describe('CSS selector for the target element')
|
||||
},
|
||||
async ({ sourceSelector, targetSelector }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.dragSelector(sourceSelector, targetSelector);
|
||||
return textResponse(`Dragged from "${sourceSelector}" to "${targetSelector}"`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_press_key',
|
||||
'Press a key or key combination in the current window',
|
||||
{
|
||||
key: z.string().describe('Key to press (e.g., "Enter", "Tab", "Control+c", "Meta+v")')
|
||||
},
|
||||
async ({ key }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.pressKey(key);
|
||||
return textResponse(`Pressed key "${key}"`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_mouse_move',
|
||||
'Move mouse to a specific position in the current window',
|
||||
{
|
||||
x: z.number().describe('X coordinate'),
|
||||
y: z.number().describe('Y coordinate')
|
||||
},
|
||||
async ({ x, y }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.mouseMove(x, y);
|
||||
return textResponse(`Moved mouse to (${x}, ${y})`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_mouse_click',
|
||||
'Click at a specific position in the current window',
|
||||
{
|
||||
x: z.number().describe('X coordinate'),
|
||||
y: z.number().describe('Y coordinate'),
|
||||
button: z.enum(['left', 'right', 'middle']).optional().describe('Mouse button to click'),
|
||||
clickCount: z.number().optional().describe('Number of clicks (1 for single, 2 for double)')
|
||||
},
|
||||
async ({ x, y, button, clickCount }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.mouseClick(x, y, { button, clickCount });
|
||||
return textResponse(`Clicked at (${x}, ${y})`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_mouse_drag',
|
||||
'Drag from one position to another in the current window',
|
||||
{
|
||||
startX: z.number().describe('Starting X coordinate'),
|
||||
startY: z.number().describe('Starting Y coordinate'),
|
||||
endX: z.number().describe('Ending X coordinate'),
|
||||
endY: z.number().describe('Ending Y coordinate')
|
||||
},
|
||||
async ({ startX, startY, endX, endY }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.mouseDrag(startX, startY, endX, endY);
|
||||
return textResponse(`Dragged from (${startX}, ${startY}) to (${endX}, ${endY})`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_select_option',
|
||||
'Select an option in a dropdown in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector for the select element'),
|
||||
value: z.union([z.string(), z.array(z.string())]).describe('Value(s) to select')
|
||||
},
|
||||
async ({ selector, value }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const selected = await driver.selectOption(selector, value);
|
||||
return textResponse(`Selected "${selected.join(', ')}" in "${selector}"`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_fill_form',
|
||||
'Fill multiple form fields at once in the current window',
|
||||
{
|
||||
fields: z.array(z.object({
|
||||
selector: z.string().describe('CSS selector for the form field'),
|
||||
value: z.string().describe('Value to fill')
|
||||
})).describe('Array of fields to fill')
|
||||
},
|
||||
async ({ fields }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.fillForm(fields);
|
||||
return textResponse(`Filled ${fields.length} form field(s)`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_console_messages',
|
||||
'Get console messages from the current window',
|
||||
async () => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const messages = await driver.getConsoleMessages();
|
||||
return textResponse(`Console messages (${messages.length}):\n${JSON.stringify(messages, null, 2)}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_wait_for_text',
|
||||
'Wait for text to appear or disappear in the current window',
|
||||
{
|
||||
text: z.string().optional().describe('Text to wait for to appear'),
|
||||
textGone: z.string().optional().describe('Text to wait for to disappear'),
|
||||
timeout: z.number().optional().describe('Timeout in milliseconds (default: 30000)')
|
||||
},
|
||||
async ({ text, textGone, timeout }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.waitForText({ text, textGone, timeout });
|
||||
return textResponse(`Waited for ${text ? `"${text}" to appear` : ''}${textGone ? `"${textGone}" to disappear` : ''}`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_wait_for_time',
|
||||
'Wait for a specified time in the current window',
|
||||
{
|
||||
seconds: z.number().describe('Time to wait in seconds')
|
||||
},
|
||||
async ({ seconds }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
await driver.waitForTime(seconds * 1000);
|
||||
return textResponse(`Waited for ${seconds} second(s)`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_verify_element_visible',
|
||||
'Verify an element is visible in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector for the element to verify')
|
||||
},
|
||||
async ({ selector }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const isVisible = await driver.verifyElementVisible(selector);
|
||||
return textResponse(isVisible ? `✓ Element "${selector}" is visible` : `✗ Element "${selector}" is NOT visible`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_verify_text_visible',
|
||||
'Verify text is visible in the current window',
|
||||
{
|
||||
text: z.string().describe('Text to verify is visible')
|
||||
},
|
||||
async ({ text }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const isVisible = await driver.verifyTextVisible(text);
|
||||
return textResponse(isVisible ? `✓ Text "${text}" is visible` : `✗ Text "${text}" is NOT visible`);
|
||||
}
|
||||
));
|
||||
|
||||
tools.push(server.tool(
|
||||
'vscode_automation_window_get_input_value',
|
||||
'Get the value of an input element in the current window',
|
||||
{
|
||||
selector: z.string().describe('CSS selector for the input element')
|
||||
},
|
||||
async ({ selector }) => {
|
||||
const app = await appService.getOrCreateApplication();
|
||||
const driver = app.code.driver;
|
||||
const value = await driver.getInputValue(selector);
|
||||
return textResponse(`Input "${selector}" value: "${value}"`);
|
||||
}
|
||||
));
|
||||
|
||||
return tools;
|
||||
}
|
||||
@@ -48,11 +48,54 @@ export async function getServer(): Promise<Server> {
|
||||
multiplexServer.addSubServer({
|
||||
subServer: playwrightClient,
|
||||
excludeTools: [
|
||||
// The page will always be opened in the context of the application,
|
||||
// so navigation and tab management is not needed.
|
||||
// Playwright MCP doesn't properly support Electron's multi-window model.
|
||||
// It uses browserContext.pages() which doesn't track Electron windows correctly.
|
||||
// We provide vscode_automation_window_* alternatives that use ElectronApplication.windows().
|
||||
|
||||
// Navigation not needed - VS Code opens its own windows
|
||||
'browser_navigate',
|
||||
'browser_navigate_back',
|
||||
'browser_tabs'
|
||||
'browser_tabs',
|
||||
|
||||
// Page interaction tools - replaced by vscode_automation_window_*
|
||||
'browser_click', // → vscode_automation_window_click
|
||||
'browser_type', // → vscode_automation_window_type
|
||||
'browser_hover', // → vscode_automation_window_hover
|
||||
'browser_drag', // → vscode_automation_window_drag
|
||||
'browser_select_option', // → vscode_automation_window_select_option
|
||||
'browser_fill_form', // → vscode_automation_window_fill_form
|
||||
'browser_press_key', // → vscode_automation_window_press_key
|
||||
|
||||
// Mouse operations - replaced by vscode_automation_window_mouse_*
|
||||
'browser_mouse_move_xy', // → vscode_automation_window_mouse_move
|
||||
'browser_mouse_click_xy', // → vscode_automation_window_mouse_click
|
||||
'browser_mouse_drag_xy', // → vscode_automation_window_mouse_drag
|
||||
|
||||
// Content capture - replaced by vscode_automation_window_*
|
||||
'browser_snapshot', // → vscode_automation_window_snapshot
|
||||
'browser_take_screenshot', // → vscode_automation_window_screenshot
|
||||
'browser_evaluate', // → vscode_automation_window_evaluate
|
||||
|
||||
// Console/debugging - replaced by vscode_automation_window_*
|
||||
'browser_console_messages', // → vscode_automation_window_console_messages
|
||||
|
||||
// Wait/timing - replaced by vscode_automation_window_*
|
||||
'browser_wait_for', // → vscode_automation_window_wait_for_text / wait_for_time
|
||||
|
||||
// Verification - replaced by vscode_automation_window_*
|
||||
'browser_verify_element_visible', // → vscode_automation_window_verify_element_visible
|
||||
'browser_verify_text_visible', // → vscode_automation_window_verify_text_visible
|
||||
'browser_verify_list_visible', // (no direct replacement - use multiple verify_text_visible)
|
||||
'browser_verify_value', // → vscode_automation_window_get_input_value
|
||||
|
||||
// Other page-dependent tools (not typically needed for VS Code testing)
|
||||
'browser_close',
|
||||
'browser_resize',
|
||||
'browser_network_requests',
|
||||
'browser_file_upload',
|
||||
'browser_handle_dialog',
|
||||
'browser_pdf_save',
|
||||
'browser_generate_locator'
|
||||
]
|
||||
});
|
||||
multiplexServer.sendToolListChanged();
|
||||
|
||||
Reference in New Issue
Block a user