/**
* VSCode Automation MCP Server - Extension Testing Tools
*
* Tools specifically for testing VSCode extensions.
*
* @author Sukarth Acharya
* @license MIT
*/
import { z } from 'zod';
import { getVSCodeDriver } from '../vscode-driver.js';
import { By, Key } from 'vscode-extension-tester';
/**
* Input schema for vscode_trigger_hover tool
*/
export const triggerHoverInputSchema = {
selector: z.string().describe('CSS selector for the element to hover over'),
waitForTooltip: z.boolean().optional().default(true).describe('Wait for tooltip to appear'),
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds'),
};
/**
* Input schema for vscode_open_context_menu tool
*/
export const openContextMenuInputSchema = {
selector: z.string().describe('CSS selector for the element to right-click'),
timeout: z.number().optional().default(5000).describe('Timeout to wait for menu'),
};
/**
* Input schema for vscode_get_menu_items tool
*/
export const getMenuItemsInputSchema = {
menuType: z.enum(['context', 'dropdown', 'any']).optional().default('any')
.describe('Type of menu to get items from'),
};
/**
* Input schema for vscode_click_menu_item tool
*/
export const clickMenuItemInputSchema = {
itemText: z.string().describe('Text of the menu item to click'),
exact: z.boolean().optional().default(false).describe('Require exact text match'),
};
/**
* Input schema for vscode_get_tooltip tool
*/
export const getTooltipInputSchema = {
selector: z.string().optional().describe('Element to get tooltip from (triggers hover if provided)'),
};
/**
* Input schema for vscode_trigger_completion tool
*/
export const triggerCompletionInputSchema = {
waitForItems: z.boolean().optional().default(true).describe('Wait for completion items to appear'),
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds'),
};
/**
* Input schema for vscode_get_completion_items tool
*/
export const getCompletionItemsInputSchema = {
limit: z.number().optional().default(50).describe('Maximum number of items to return'),
};
/**
* Input schema for vscode_select_completion_item tool
*/
export const selectCompletionItemInputSchema = {
itemLabel: z.string().describe('Label of the completion item to select'),
exact: z.boolean().optional().default(false).describe('Require exact text match'),
};
/**
* Trigger hover on an element and optionally wait for tooltip
*/
export async function triggerHover(input: {
selector: string;
waitForTooltip?: boolean;
timeout?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const waitForTooltip = input.waitForTooltip !== false;
const timeout = input.timeout || 5000;
try {
const element = await webDriver.findElement(By.css(input.selector));
// Move to element to trigger hover
const actions = webDriver.actions({ async: true });
await actions.move({ origin: element }).perform();
let tooltipText: string | null = null;
if (waitForTooltip) {
// Wait for tooltip to appear
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const tooltip = await webDriver.executeScript<string | null>(
`
const tooltips = document.querySelectorAll('.monaco-hover, .monaco-tooltip, [role="tooltip"], .hover-contents');
for (const t of tooltips) {
if (t.offsetParent !== null && t.textContent?.trim()) {
return t.textContent.trim();
}
}
return null;
`
);
if (tooltip) {
tooltipText = tooltip;
break;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
hovered: true,
tooltipFound: tooltipText !== null,
tooltipText,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Open context menu on an element
*/
export async function openContextMenu(input: {
selector: string;
timeout?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const timeout = input.timeout || 5000;
try {
const element = await webDriver.findElement(By.css(input.selector));
// Right-click to open context menu
const actions = webDriver.actions({ async: true });
await actions.contextClick(element).perform();
// Wait for context menu to appear
const startTime = Date.now();
let menuFound = false;
while (Date.now() - startTime < timeout) {
const hasMenu = await webDriver.executeScript<boolean>(
`
const menus = document.querySelectorAll('.monaco-menu, .context-view, [role="menu"]');
return Array.from(menus).some(m => m.offsetParent !== null);
`
);
if (hasMenu) {
menuFound = true;
break;
}
await new Promise(resolve => setTimeout(resolve, 50));
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: menuFound,
selector: input.selector,
menuOpened: menuFound,
message: menuFound ? 'Context menu opened' : 'Context menu did not appear within timeout',
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Get menu items from visible menus
*/
export async function getMenuItems(input: {
menuType?: 'context' | 'dropdown' | 'any';
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const menuType = input.menuType || 'any';
try {
const result = await webDriver.executeScript<string>(
`
const menuType = arguments[0];
const items = [];
// Selector based on menu type
let selectors = [];
if (menuType === 'context' || menuType === 'any') {
selectors.push('.monaco-menu .action-item', '.context-view .action-item');
}
if (menuType === 'dropdown' || menuType === 'any') {
selectors.push('.monaco-dropdown .action-item', '.select-box-dropdown-list-container .monaco-list-row');
}
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach((item, index) => {
if (item.offsetParent === null) return;
const label = item.querySelector('.action-label, .option-text');
const text = label?.textContent?.trim() || item.textContent?.trim();
const isDisabled = item.classList.contains('disabled') || item.getAttribute('aria-disabled') === 'true';
const isSeparator = item.classList.contains('separator');
const hasSubmenu = item.querySelector('.submenu-indicator') !== null ||
item.getAttribute('aria-haspopup') === 'true';
if (text || isSeparator) {
items.push({
index,
text: text || '---',
isSeparator,
isDisabled,
hasSubmenu,
selector: \`\${selector}:nth-child(\${index + 1})\`
});
}
});
});
return JSON.stringify(items);
`,
menuType
);
const items = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
menuType,
count: items.length,
items,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Click a menu item by text
*/
export async function clickMenuItem(input: {
itemText: string;
exact?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const exact = input.exact === true;
try {
const clicked = await webDriver.executeScript<boolean>(
`
const targetText = arguments[0];
const exact = arguments[1];
const items = document.querySelectorAll('.monaco-menu .action-item, .context-view .action-item, [role="menuitem"]');
for (const item of items) {
if (item.offsetParent === null) continue;
const text = item.textContent?.trim();
if (!text) continue;
const matches = exact ? text === targetText : text.toLowerCase().includes(targetText.toLowerCase());
if (matches && !item.classList.contains('disabled')) {
const clickable = item.querySelector('.action-label, .monaco-action-bar a') || item;
clickable.click();
return true;
}
}
return false;
`,
input.itemText,
exact
);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: clicked,
itemText: input.itemText,
clicked,
message: clicked ? 'Menu item clicked' : 'Menu item not found or is disabled',
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Get visible tooltip content
*/
export async function getTooltip(input: {
selector?: string;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
// If selector provided, hover over it first
if (input.selector) {
const element = await webDriver.findElement(By.css(input.selector));
const actions = webDriver.actions({ async: true });
await actions.move({ origin: element }).perform();
// Wait for tooltip
await new Promise(resolve => setTimeout(resolve, 500));
}
const result = await webDriver.executeScript<string>(
`
const tooltips = [];
const selectors = [
'.monaco-hover',
'.monaco-hover-content',
'.monaco-tooltip',
'[role="tooltip"]',
'.hover-contents',
'.parameter-hints-widget',
'.suggest-widget .details'
];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(t => {
if (t.offsetParent !== null) {
tooltips.push({
type: selector.replace(/[.\\[\\]"=]/g, ''),
text: t.textContent?.trim(),
html: t.innerHTML?.substring(0, 500)
});
}
});
});
return JSON.stringify(tooltips);
`
);
const tooltips = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
count: tooltips.length,
tooltips,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Trigger IntelliSense/completion
*/
export async function triggerCompletion(input: {
waitForItems?: boolean;
timeout?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const waitForItems = input.waitForItems !== false;
const timeout = input.timeout || 5000;
try {
// Trigger completion with Ctrl+Space
const actions = webDriver.actions({ async: true });
await actions.keyDown(Key.CONTROL).sendKeys(' ').keyUp(Key.CONTROL).perform();
let widgetFound = false;
let itemCount = 0;
if (waitForItems) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const result = await webDriver.executeScript<string>(
`
const widget = document.querySelector('.suggest-widget:not(.hidden), .editor-widget.suggest-widget');
if (widget && widget.offsetParent !== null) {
const items = widget.querySelectorAll('.monaco-list-row');
return JSON.stringify({ found: true, count: items.length });
}
return JSON.stringify({ found: false, count: 0 });
`
);
const parsed = JSON.parse(result);
if (parsed.found && parsed.count > 0) {
widgetFound = true;
itemCount = parsed.count;
break;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
triggered: true,
completionWidgetVisible: widgetFound,
itemCount,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Get completion/IntelliSense items
*/
export async function getCompletionItems(input: {
limit?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const limit = input.limit || 50;
try {
const result = await webDriver.executeScript<string>(
`
const limit = arguments[0];
const items = [];
const widget = document.querySelector('.suggest-widget:not(.hidden), .editor-widget.suggest-widget');
if (!widget || widget.offsetParent === null) {
return JSON.stringify({ visible: false, items: [] });
}
const rows = widget.querySelectorAll('.monaco-list-row');
rows.forEach((row, index) => {
if (index >= limit) return;
const label = row.querySelector('.label-name, .monaco-icon-label-container .label-name');
const icon = row.querySelector('.suggest-icon, .monaco-icon-label::before');
const detail = row.querySelector('.details-label, .monaco-icon-description-container');
const signature = row.querySelector('.signature-label');
items.push({
index,
label: label?.textContent?.trim() || row.textContent?.trim(),
detail: detail?.textContent?.trim(),
signature: signature?.textContent?.trim(),
kind: icon?.classList.toString() || '',
isSelected: row.classList.contains('focused') || row.getAttribute('aria-selected') === 'true'
});
});
return JSON.stringify({ visible: true, items });
`,
limit
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
completionWidgetVisible: parsed.visible,
count: parsed.items.length,
items: parsed.items,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Select a completion item
*/
export async function selectCompletionItem(input: {
itemLabel: string;
exact?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const exact = input.exact === true;
try {
const found = await webDriver.executeScript<boolean>(
`
const targetLabel = arguments[0];
const exact = arguments[1];
const widget = document.querySelector('.suggest-widget:not(.hidden)');
if (!widget) return false;
const rows = widget.querySelectorAll('.monaco-list-row');
for (const row of rows) {
const label = row.querySelector('.label-name, .monaco-icon-label-container');
const text = label?.textContent?.trim() || row.textContent?.trim();
const matches = exact ? text === targetLabel : text?.toLowerCase().includes(targetLabel.toLowerCase());
if (matches) {
row.click();
return true;
}
}
return false;
`,
input.itemLabel,
exact
);
if (found) {
// Press Enter to accept
await new Promise(resolve => setTimeout(resolve, 100));
const actions = webDriver.actions({ async: true });
await actions.sendKeys(Key.ENTER).perform();
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: found,
itemLabel: input.itemLabel,
selected: found,
message: found ? 'Completion item selected and accepted' : 'Completion item not found',
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Get problems/diagnostics from Problems panel
*/
export async function getProblemsPanel(_input: Record<string, never>): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const problems = [];
// Look for problems panel items
const problemRows = document.querySelectorAll('.markers-panel .monaco-list-row, .problem-widget .monaco-list-row');
problemRows.forEach((row, index) => {
const severity = row.querySelector('.codicon-error, .codicon-warning, .codicon-info');
const severityClass = severity?.classList.toString() || '';
let severityLevel = 'unknown';
if (severityClass.includes('error')) severityLevel = 'error';
else if (severityClass.includes('warning')) severityLevel = 'warning';
else if (severityClass.includes('info')) severityLevel = 'info';
const message = row.querySelector('.marker-message, .problem-message');
const source = row.querySelector('.marker-source, .problem-source');
const file = row.querySelector('.marker-file, .file-name');
const location = row.querySelector('.marker-line, .problem-location');
problems.push({
index,
severity: severityLevel,
message: message?.textContent?.trim(),
source: source?.textContent?.trim(),
file: file?.textContent?.trim(),
location: location?.textContent?.trim()
});
});
// Also check status bar for summary
const statusBar = document.querySelector('.statusbar-item[id*="marker"], [aria-label*="Problems"]');
const summary = statusBar?.textContent?.trim();
return JSON.stringify({ problems, summary });
`
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
count: parsed.problems.length,
summary: parsed.summary,
problems: parsed.problems,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
export const getProblemsPanelInputSchema = {};
/**
* Get definition info for symbol at cursor
*/
export async function goToDefinition(_input: Record<string, never>): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
// Get current position before
const beforeResult = await webDriver.executeScript<string>(
`
const editor = document.querySelector('.monaco-editor .view-line.current, .editor-instance');
return JSON.stringify({
activeElement: document.activeElement?.className,
currentLine: editor?.textContent?.substring(0, 100)
});
`
);
// Trigger Go to Definition with F12
const actions = webDriver.actions({ async: true });
await actions.sendKeys(Key.F12).perform();
// Wait for navigation
await new Promise(resolve => setTimeout(resolve, 500));
const afterResult = await webDriver.executeScript<string>(
`
const title = document.querySelector('.title .label-name, .tabs-container .tab.active .label-name');
const editor = document.querySelector('.monaco-editor .view-line.current, .editor-instance');
return JSON.stringify({
currentFile: title?.textContent?.trim(),
navigated: true
});
`
);
const parsed = JSON.parse(afterResult);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
action: 'goToDefinition',
...parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
export const goToDefinitionInputSchema = {};
/**
* Trigger signature help (parameter info)
*/
export async function triggerSignatureHelp(_input: Record<string, never>): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
// Trigger with Ctrl+Shift+Space
const actions = webDriver.actions({ async: true });
await actions
.keyDown(Key.CONTROL)
.keyDown(Key.SHIFT)
.sendKeys(' ')
.keyUp(Key.SHIFT)
.keyUp(Key.CONTROL)
.perform();
// Wait for signature help
await new Promise(resolve => setTimeout(resolve, 300));
const result = await webDriver.executeScript<string>(
`
const widget = document.querySelector('.parameter-hints-widget:not(.hidden)');
if (!widget || widget.offsetParent === null) {
return JSON.stringify({ visible: false });
}
const signature = widget.querySelector('.signature');
const params = widget.querySelectorAll('.parameter');
const activeParam = widget.querySelector('.parameter.active');
const docs = widget.querySelector('.documentation');
return JSON.stringify({
visible: true,
signature: signature?.textContent?.trim(),
parameterCount: params.length,
activeParameter: activeParam?.textContent?.trim(),
documentation: docs?.textContent?.trim()
});
`
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
...parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
export const triggerSignatureHelpInputSchema = {};