/**
* VSCode Automation MCP Server - Performance & Debug Tools
*
* Tools for performance monitoring, console logs, and debugging.
*
* @author Sukarth Acharya
* @license MIT
*/
import { z } from 'zod';
import { getVSCodeDriver } from '../vscode-driver.js';
/**
* Input schema for vscode_get_console_logs tool
*/
export const getConsoleLogsInputSchema = {
level: z.enum(['all', 'log', 'info', 'warn', 'error', 'debug']).optional().default('all')
.describe('Filter by log level'),
limit: z.number().optional().default(100).describe('Maximum number of logs to return'),
since: z.number().optional().describe('Only return logs after this timestamp'),
};
/**
* Input schema for vscode_get_output_channels tool
*/
export const getOutputChannelsInputSchema = {};
/**
* Input schema for vscode_get_output_channel_content tool
*/
export const getOutputChannelContentInputSchema = {
channelName: z.string().describe('Name of the output channel to read'),
lines: z.number().optional().default(200).describe('Number of lines to return (from end)'),
};
/**
* Input schema for vscode_get_performance_metrics tool
*/
export const getPerformanceMetricsInputSchema = {};
/**
* Input schema for vscode_clear_console tool
*/
export const clearConsoleInputSchema = {};
/**
* Input schema for vscode_get_extension_logs tool
*/
export const getExtensionLogsInputSchema = {
extensionId: z.string().optional().describe('Filter logs by extension ID'),
limit: z.number().optional().default(100).describe('Maximum number of log entries'),
};
// Store for captured console logs
let capturedLogs: Array<{
level: string;
message: string;
timestamp: number;
args?: unknown[];
}> = [];
let isCapturing = false;
/**
* Start capturing console logs (internal helper)
*/
async function ensureLogCapture(webDriver: { executeScript: <T>(script: string) => Promise<T> }): Promise<void> {
if (isCapturing) return;
await webDriver.executeScript<void>(
`
if (!window.__mcpConsoleLogs) {
window.__mcpConsoleLogs = [];
window.__mcpMaxLogs = 1000;
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug
};
const capture = (level, args) => {
const entry = {
level,
timestamp: Date.now(),
message: Array.from(args).map(a => {
try {
if (typeof a === 'string') return a;
if (a instanceof Error) return a.stack || a.message;
return JSON.stringify(a);
} catch (e) {
return String(a);
}
}).join(' ')
};
window.__mcpConsoleLogs.push(entry);
// Keep only last N logs
if (window.__mcpConsoleLogs.length > window.__mcpMaxLogs) {
window.__mcpConsoleLogs = window.__mcpConsoleLogs.slice(-window.__mcpMaxLogs);
}
};
console.log = (...args) => { capture('log', args); originalConsole.log.apply(console, args); };
console.info = (...args) => { capture('info', args); originalConsole.info.apply(console, args); };
console.warn = (...args) => { capture('warn', args); originalConsole.warn.apply(console, args); };
console.error = (...args) => { capture('error', args); originalConsole.error.apply(console, args); };
console.debug = (...args) => { capture('debug', args); originalConsole.debug.apply(console, args); };
}
`
);
isCapturing = true;
}
/**
* Get captured console logs
*/
export async function getConsoleLogs(input: {
level?: 'all' | 'log' | 'info' | 'warn' | 'error' | 'debug';
limit?: number;
since?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const level = input.level || 'all';
const limit = input.limit || 100;
try {
await ensureLogCapture(webDriver);
const result = await webDriver.executeScript<string>(
`
const logs = window.__mcpConsoleLogs || [];
const level = arguments[0];
const limit = arguments[1];
const since = arguments[2];
let filtered = logs;
if (level !== 'all') {
filtered = filtered.filter(l => l.level === level);
}
if (since) {
filtered = filtered.filter(l => l.timestamp > since);
}
return JSON.stringify(filtered.slice(-limit));
`,
level,
limit,
input.since ?? null
);
const logs = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
level,
count: logs.length,
logs: logs.map((l: { level: string; timestamp: number; message: string }) => ({
...l,
time: new Date(l.timestamp).toISOString(),
})),
}, 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),
}],
};
}
}
/**
* Clear captured console logs
*/
export async function clearConsole(_input: Record<string, never>): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
await webDriver.executeScript<void>(
`window.__mcpConsoleLogs = [];`
);
capturedLogs = [];
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Console logs cleared',
}, 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 list of available output channels
*/
export async function getOutputChannels(_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 channels = [];
// Try to find output channel selector
const selector = document.querySelector('.monaco-select-box, .output-view-selector');
if (selector) {
const options = selector.querySelectorAll('option');
options.forEach((opt, i) => {
channels.push({
index: i,
name: opt.textContent?.trim() || opt.value,
value: opt.value
});
});
}
// Also check the output panel tabs
const tabs = document.querySelectorAll('.output-view .monaco-action-bar .action-item');
tabs.forEach((tab, i) => {
const name = tab.getAttribute('aria-label') || tab.textContent?.trim();
if (name && !channels.find(c => c.name === name)) {
channels.push({
index: channels.length,
name,
value: name
});
}
});
// Check for known VSCode output channels in DOM
const outputPanelTitle = document.querySelector('.output-view .title, .panel-title');
if (outputPanelTitle) {
const currentChannel = outputPanelTitle.textContent?.replace('Output - ', '').trim();
if (currentChannel && !channels.find(c => c.name === currentChannel)) {
channels.push({
index: 0,
name: currentChannel,
value: currentChannel,
current: true
});
}
}
return JSON.stringify(channels);
`
);
const channels = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
count: channels.length,
channels,
note: 'Output channels may require the Output panel to be visible. Use vscode_execute_command with "workbench.action.output.toggleOutput" to open it.',
}, 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 content from an output channel
*/
export async function getOutputChannelContent(input: {
channelName: string;
lines?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const lines = input.lines || 200;
try {
const result = await webDriver.executeScript<string>(
`
const channelName = arguments[0];
const maxLines = arguments[1];
// Try to read from the output panel
const outputContainer = document.querySelector('.output-view .editor-instance, .output-view .monaco-editor');
if (!outputContainer) {
return JSON.stringify({
error: 'Output panel not found. Make sure the Output panel is visible.',
suggestion: 'Execute command "workbench.action.output.toggleOutput" first'
});
}
// Get text from monaco editor
const lines = outputContainer.querySelectorAll('.view-line');
const content = Array.from(lines).map(l => l.textContent).join('\\n');
// Also try to get from textarea/text content
const text = content || outputContainer.textContent || '';
const allLines = text.split('\\n');
const lastLines = allLines.slice(-maxLines);
return JSON.stringify({
channelName,
totalLines: allLines.length,
returnedLines: lastLines.length,
content: lastLines.join('\\n')
});
`,
input.channelName,
lines
);
const parsed = JSON.parse(result);
if (parsed.error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
...parsed,
}, null, 2),
}],
};
}
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),
}],
};
}
}
/**
* Get performance metrics
*/
export async function getPerformanceMetrics(_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 metrics = {};
// Performance timing
if (window.performance) {
const timing = performance.timing;
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
metrics.navigation = {
loadTime: navigation.loadEventEnd - navigation.startTime,
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.startTime,
domInteractive: navigation.domInteractive - navigation.startTime,
type: navigation.type
};
}
// Memory (if available, Chrome only)
if (performance.memory) {
metrics.memory = {
usedJSHeapSize: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + ' MB',
totalJSHeapSize: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + ' MB',
jsHeapSizeLimit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + ' MB',
usagePercent: Math.round((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100) + '%'
};
}
// Resource timing summary
const resources = performance.getEntriesByType('resource');
const resourceSummary = {
total: resources.length,
byType: {}
};
resources.forEach(r => {
const type = r.initiatorType;
if (!resourceSummary.byType[type]) {
resourceSummary.byType[type] = { count: 0, totalSize: 0, totalTime: 0 };
}
resourceSummary.byType[type].count++;
resourceSummary.byType[type].totalTime += r.duration;
});
metrics.resources = resourceSummary;
}
// DOM stats
metrics.dom = {
totalElements: document.getElementsByTagName('*').length,
maxDepth: (function getMaxDepth(el, depth) {
if (!el.children.length) return depth;
return Math.max(...Array.from(el.children).map(c => getMaxDepth(c, depth + 1)));
})(document.body, 0),
scripts: document.scripts.length,
stylesheets: document.styleSheets.length,
images: document.images.length,
iframes: document.querySelectorAll('iframe').length
};
// Animation frame rate estimate
metrics.hasAnimations = document.getAnimations?.().length || 0;
return JSON.stringify(metrics);
`
);
const metrics = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
timestamp: new Date().toISOString(),
metrics,
}, 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 extension host logs and extension-specific logging
*/
export async function getExtensionLogs(input: {
extensionId?: string;
limit?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const limit = input.limit || 100;
try {
// First, check console logs for extension-related entries
await ensureLogCapture(webDriver);
const result = await webDriver.executeScript<string>(
`
const extensionId = arguments[0];
const limit = arguments[1];
const logs = window.__mcpConsoleLogs || [];
// Filter for extension-related logs
let filtered = logs.filter(l => {
if (!extensionId) {
// Look for common extension log patterns
return l.message.includes('extension') ||
l.message.includes('Extension') ||
l.message.includes('[') && l.message.includes(']');
}
return l.message.includes(extensionId);
});
// Also check for errors that might be extension-related
const errorLogs = logs.filter(l => l.level === 'error').slice(-20);
return JSON.stringify({
extensionLogs: filtered.slice(-limit),
recentErrors: errorLogs
});
`,
input.extensionId ?? null,
limit
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
extensionId: input.extensionId || 'all',
logCount: parsed.extensionLogs.length,
errorCount: parsed.recentErrors.length,
logs: parsed.extensionLogs.map((l: { level: string; timestamp: number; message: string }) => ({
...l,
time: new Date(l.timestamp).toISOString(),
})),
recentErrors: parsed.recentErrors.map((l: { level: string; timestamp: number; message: string }) => ({
...l,
time: new Date(l.timestamp).toISOString(),
})),
note: 'For full extension host logs, open Output panel and select "Extension Host" channel',
}, 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 Developer Tools info (what would be in DevTools)
*/
export async function getDevToolsInfo(_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 info = {};
// Window info
info.window = {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
location: window.location.href
};
// Document info
info.document = {
title: document.title,
readyState: document.readyState,
characterSet: document.characterSet,
contentType: document.contentType,
lastModified: document.lastModified
};
// Navigator info
info.navigator = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
onLine: navigator.onLine,
hardwareConcurrency: navigator.hardwareConcurrency
};
// VSCode specific
info.vscode = {
hasMonaco: typeof window.monaco !== 'undefined',
hasAcquireVsCodeApi: typeof acquireVsCodeApi !== 'undefined'
};
// Check for global APIs
info.globalAPIs = {
monaco: !!window.monaco,
vscode: !!window.vscode,
require: typeof require !== 'undefined'
};
// Storage info
try {
info.storage = {
localStorageKeys: Object.keys(localStorage).length,
sessionStorageKeys: Object.keys(sessionStorage).length
};
} catch (e) {
info.storage = { error: 'Access denied' };
}
return JSON.stringify(info);
`
);
const info = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
...info,
}, 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 getDevToolsInfoInputSchema = {};