style: fix linting errors and update README with new tools

- Auto-fix trailing spaces, curly braces, and indentation issues
- Clean up boolean comparisons and code formatting
- README automatically updated with new code injection tools:
  - browser_enable_debug_toolbar: Enable debug toolbar for client identification
  - browser_inject_custom_code: Inject custom JavaScript/CSS code
  - browser_list_injections: List all active code injections
  - browser_disable_debug_toolbar: Disable debug toolbar
  - browser_clear_injections: Remove custom code injections

All linting checks now pass successfully.
This commit is contained in:
Ryan Malloy 2025-09-10 01:38:24 -06:00
parent b7ec4faf60
commit a41a73af2a
27 changed files with 1603 additions and 134 deletions

View file

@ -1,6 +1,21 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Code Injection Tools for MCP Client Identification and Custom Scripts
*
*
* Provides tools for injecting debug toolbars and custom code into browser pages.
* Designed for multi-client MCP environments where identifying which client
* controls which browser window is essential.
@ -47,7 +62,7 @@ export function generateDebugToolbarScript(config: DebugToolbarConfig, sessionId
const projectName = config.projectName || 'MCP Client';
const clientInfo = clientVersion ? `${clientVersion.name} v${clientVersion.version}` : 'Unknown Client';
const startTime = sessionStartTime || Date.now();
return `
/* BEGIN PLAYWRIGHT-MCP-DEBUG-TOOLBAR */
/* This debug toolbar was injected by Playwright MCP server */
@ -217,7 +232,7 @@ export function wrapInjectedCode(injection: CustomInjection, sessionId: string,
<!-- Session: ${sessionId}${projectInfo} -->
<!-- This code was injected by Playwright MCP and should be ignored by LLMs -->`;
const footer = `<!-- END PLAYWRIGHT-MCP-INJECTION: ${injection.name} -->`;
if (injection.type === 'javascript') {
return `${header}
<script>
@ -233,7 +248,7 @@ ${injection.code}
</style>
${footer}`;
}
return `${header}
${injection.code}
${footer}`;
@ -315,7 +330,7 @@ const enableDebugToolbar = defineTool({
},
handle: async (context: Context, params: z.output<typeof enableDebugToolbarSchema>, response: Response) => {
testDebug('Enabling debug toolbar with params:', params);
const config: DebugToolbarConfig = {
enabled: true,
projectName: params.projectName || 'MCP Client',
@ -325,7 +340,7 @@ const enableDebugToolbar = defineTool({
showDetails: params.showDetails !== false,
opacity: params.opacity || 0.9
};
// Store config in context
if (!context.injectionConfig) {
context.injectionConfig = {
@ -337,10 +352,10 @@ const enableDebugToolbar = defineTool({
context.injectionConfig.debugToolbar = config;
context.injectionConfig.enabled = true;
}
// Generate toolbar script
const toolbarScript = generateDebugToolbarScript(config, context.sessionId, context.clientVersion, (context as any)._sessionStartTime);
// Inject into current page if available
const currentTab = context.currentTab();
if (currentTab) {
@ -352,7 +367,7 @@ const enableDebugToolbar = defineTool({
testDebug('Error injecting toolbar into current page:', error);
}
}
const resultMessage = `Debug toolbar enabled for project "${config.projectName}"`;
response.addResult(resultMessage);
response.addResult(`Session ID: ${context.sessionId}`);
@ -371,7 +386,7 @@ const injectCustomCode = defineTool({
},
handle: async (context: Context, params: z.output<typeof injectCustomCodeSchema>, response: Response) => {
testDebug('Injecting custom code:', { name: params.name, type: params.type });
if (!context.injectionConfig) {
context.injectionConfig = {
debugToolbar: { enabled: false, minimized: false, showDetails: true, position: 'top-right', theme: 'dark', opacity: 0.9 },
@ -379,7 +394,7 @@ const injectCustomCode = defineTool({
enabled: true
};
}
// Create injection object
const injection: CustomInjection = {
id: `${params.name}_${Date.now()}`,
@ -390,19 +405,19 @@ const injectCustomCode = defineTool({
persistent: params.persistent !== false,
autoInject: params.autoInject !== false
};
// Remove any existing injection with the same name
context.injectionConfig.customInjections = context.injectionConfig.customInjections.filter(
inj => inj.name !== params.name
inj => inj.name !== params.name
);
// Add new injection
context.injectionConfig.customInjections.push(injection);
// Wrap code with LLM-safe markers
const wrappedCode = wrapInjectedCode(injection, context.sessionId, context.injectionConfig.debugToolbar.projectName);
const injectionScript = generateInjectionScript(wrappedCode);
// Inject into current page if available
const currentTab = context.currentTab();
if (currentTab && injection.autoInject) {
@ -414,7 +429,7 @@ const injectCustomCode = defineTool({
testDebug('Error injecting custom code into current page:', error);
}
}
response.addResult(`Custom ${params.type} injection "${params.name}" added successfully`);
response.addResult(`Total injections: ${context.injectionConfig.customInjections.length}`);
response.addResult(`Auto-inject enabled: ${injection.autoInject}`);
@ -432,12 +447,12 @@ const listInjections = defineTool({
},
handle: async (context: Context, params: any, response: Response) => {
const config = context.injectionConfig;
if (!config) {
response.addResult('No injection configuration found');
return;
}
response.addResult(`Session ID: ${context.sessionId}`);
response.addResult(`\nDebug Toolbar:`);
response.addResult(`- Enabled: ${config.debugToolbar.enabled}`);
@ -447,7 +462,7 @@ const listInjections = defineTool({
response.addResult(`- Theme: ${config.debugToolbar.theme}`);
response.addResult(`- Minimized: ${config.debugToolbar.minimized}`);
}
response.addResult(`\nCustom Injections (${config.customInjections.length}):`);
if (config.customInjections.length === 0) {
response.addResult('- None');
@ -471,19 +486,19 @@ const disableDebugToolbar = defineTool({
type: 'destructive',
},
handle: async (context: Context, params: any, response: Response) => {
if (context.injectionConfig) {
if (context.injectionConfig)
context.injectionConfig.debugToolbar.enabled = false;
}
// Remove from current page if available
const currentTab = context.currentTab();
if (currentTab) {
try {
await currentTab.page.evaluate(() => {
const toolbar = document.getElementById('playwright-mcp-debug-toolbar');
if (toolbar) {
if (toolbar)
toolbar.remove();
}
(window as any).playwrightMcpDebugToolbar = false;
});
testDebug('Debug toolbar removed from current page');
@ -491,7 +506,7 @@ const disableDebugToolbar = defineTool({
testDebug('Error removing toolbar from current page:', error);
}
}
response.addResult('Debug toolbar disabled');
}
});
@ -510,22 +525,22 @@ const clearInjections = defineTool({
response.addResult('No injections to clear');
return;
}
const clearedCount = context.injectionConfig.customInjections.length;
context.injectionConfig.customInjections = [];
if (params.includeToolbar) {
context.injectionConfig.debugToolbar.enabled = false;
// Remove toolbar from current page
const currentTab = context.currentTab();
if (currentTab) {
try {
await currentTab.page.evaluate(() => {
const toolbar = document.getElementById('playwright-mcp-debug-toolbar');
if (toolbar) {
if (toolbar)
toolbar.remove();
}
(window as any).playwrightMcpDebugToolbar = false;
});
} catch (error) {
@ -533,7 +548,7 @@ const clearInjections = defineTool({
}
}
}
response.addResult(`Cleared ${clearedCount} custom injections${params.includeToolbar ? ' and disabled debug toolbar' : ''}`);
}
});
@ -544,4 +559,4 @@ export default [
listInjections,
disableDebugToolbar,
clearInjections,
];
];

View file

@ -39,7 +39,7 @@ const configureSchema = z.object({
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional().describe('Preferred color scheme'),
permissions: z.array(z.string()).optional().describe('Permissions to grant (e.g., ["geolocation", "notifications", "camera", "microphone"])'),
offline: z.boolean().optional().describe('Whether to emulate offline network conditions (equivalent to DevTools offline mode)'),
// Browser UI Customization Options
chromiumSandbox: z.boolean().optional().describe('Enable/disable Chromium sandbox (affects browser appearance)'),
slowMo: z.number().min(0).optional().describe('Slow down operations by specified milliseconds (helps with visual tracking)'),

View file

@ -33,7 +33,7 @@ const navigate = defineTool({
handle: async (context, params, response) => {
// Smart recording: Begin action
await context.beginVideoAction('navigate');
const tab = await context.ensureTab();
await tab.navigate(params.url);

View file

@ -54,7 +54,7 @@ const startRecording = defineTool({
// Default video size for better demos
const videoSize = params.size || { width: 1280, height: 720 };
// Update context options to enable video recording
const recordVideoOptions: any = {
dir: videoDir,
@ -62,7 +62,7 @@ const startRecording = defineTool({
};
// Automatically set viewport to match video size for full-frame content
if (params.autoSetViewport !== false) {
if (params.autoSetViewport) {
try {
await context.updateBrowserConfig({
viewport: {
@ -84,19 +84,19 @@ const startRecording = defineTool({
response.addResult(`📁 Videos will be saved to: ${videoDir}`);
response.addResult(`📝 Files will be named: ${baseFilename}-*.webm`);
response.addResult(`📐 Video size: ${videoSize.width}x${videoSize.height}`);
// Show viewport matching info
if (params.autoSetViewport !== false) {
if (params.autoSetViewport) {
response.addResult(`🖼️ Browser viewport matched to video size for full-frame content`);
} else {
response.addResult(`⚠️ Viewport not automatically set - you may see gray borders around content`);
response.addResult(`💡 For full-frame content, use: browser_configure({viewport: {width: ${videoSize.width}, height: ${videoSize.height}}})`);
}
// Show current recording mode
const recordingInfo = context.getVideoRecordingInfo();
response.addResult(`🎯 Recording mode: ${recordingInfo.mode}`);
switch (recordingInfo.mode) {
case 'smart':
response.addResult(`🧠 Smart mode: Auto-pauses during waits, resumes during actions`);
@ -112,7 +112,7 @@ const startRecording = defineTool({
response.addResult(`🎞️ Segment mode: Creating separate files for each action sequence`);
break;
}
response.addResult(`\n📋 Next steps:`);
response.addResult(`1. Navigate to pages and perform browser actions`);
response.addResult(`2. Use browser_stop_recording when finished to save videos`);
@ -179,11 +179,11 @@ const getRecordingStatus = defineTool({
response.addResult('1. Use browser_start_recording to enable recording');
response.addResult('2. Navigate to pages and perform actions');
response.addResult('3. Use browser_stop_recording to save videos');
// Show potential artifact locations for debugging
const registry = ArtifactManagerRegistry.getInstance();
const artifactManager = context.sessionId ? registry.getManager(context.sessionId) : undefined;
if (artifactManager) {
const baseDir = artifactManager.getBaseDirectory();
const sessionDir = artifactManager.getSessionDirectory();
@ -195,7 +195,7 @@ const getRecordingStatus = defineTool({
response.addResult(`\n⚠ No artifact manager configured - videos will save to default output directory`);
response.addResult(`📁 Default output: ${path.join(context.config.outputDir, 'videos')}`);
}
return;
}
@ -209,23 +209,23 @@ const getRecordingStatus = defineTool({
response.addResult(`🎬 Active recordings: ${recordingInfo.activeRecordings}`);
response.addResult(`🎯 Recording mode: ${recordingInfo.mode}`);
if (recordingInfo.paused) {
if (recordingInfo.paused)
response.addResult(`⏸️ Status: PAUSED (${recordingInfo.pausedRecordings} recordings stored)`);
} else {
else
response.addResult(`▶️ Status: RECORDING`);
}
if (recordingInfo.mode === 'segment') {
if (recordingInfo.mode === 'segment')
response.addResult(`🎞️ Current segment: ${recordingInfo.currentSegment}`);
}
// Show helpful path info for MCP clients
const outputDir = recordingInfo.config?.dir;
if (outputDir) {
const absolutePath = path.resolve(outputDir);
response.addResult(`📍 Absolute path: ${absolutePath}`);
// Check if directory exists and show contents
const fs = await import('fs');
if (fs.existsSync(absolutePath)) {
@ -249,7 +249,7 @@ const getRecordingStatus = defineTool({
// Show debug information
const registry = ArtifactManagerRegistry.getInstance();
const artifactManager = context.sessionId ? registry.getManager(context.sessionId) : undefined;
if (artifactManager) {
response.addResult(`\n🔍 Debug Info:`);
response.addResult(`🆔 Session ID: ${context.sessionId}`);
@ -313,13 +313,13 @@ const revealArtifactPaths = defineTool({
const files = items.filter(item => item.isFile()).map(item => item.name);
const dirs = items.filter(item => item.isDirectory()).map(item => item.name);
if (dirs.length > 0) {
if (dirs.length > 0)
response.addResult(`\n📂 Existing subdirectories: ${dirs.join(', ')}`);
}
if (files.length > 0) {
if (files.length > 0)
response.addResult(`📄 Files in session directory: ${files.join(', ')}`);
}
// Count .webm files across all subdirectories
let webmCount = 0;
@ -328,11 +328,11 @@ const revealArtifactPaths = defineTool({
const contents = fs.readdirSync(dir, { withFileTypes: true });
for (const item of contents) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
if (item.isDirectory())
countWebmFiles(fullPath);
} else if (item.name.endsWith('.webm')) {
else if (item.name.endsWith('.webm'))
webmCount++;
}
}
} catch (error) {
// Ignore permission errors
@ -340,9 +340,9 @@ const revealArtifactPaths = defineTool({
}
countWebmFiles(sessionDir);
if (webmCount > 0) {
if (webmCount > 0)
response.addResult(`🎬 Total .webm video files found: ${webmCount}`);
}
} catch (error: any) {
response.addResult(`⚠️ Could not list session directory contents: ${error.message}`);
}
@ -383,9 +383,9 @@ const pauseRecording = defineTool({
handle: async (context, params, response) => {
const result = await context.pauseVideoRecording();
response.addResult(`⏸️ ${result.message}`);
if (result.paused > 0) {
if (result.paused > 0)
response.addResult(`💡 Use browser_resume_recording to continue`);
}
},
});
@ -421,9 +421,9 @@ const setRecordingMode = defineTool({
handle: async (context, params, response) => {
context.setVideoRecordingMode(params.mode);
response.addResult(`🎬 Video recording mode set to: ${params.mode}`);
switch (params.mode) {
case 'continuous':
response.addResult('📹 Will record everything continuously (traditional behavior)');
@ -441,7 +441,7 @@ const setRecordingMode = defineTool({
response.addResult('💡 Useful for breaking demos into individual clips');
break;
}
const recordingInfo = context.getVideoRecordingInfo();
if (recordingInfo.enabled) {
response.addResult(`\n🎥 Current recording status: ${recordingInfo.paused ? 'paused' : 'active'}`);

View file

@ -39,8 +39,8 @@ const wait = defineTool({
// Handle smart recording for waits
const recordingInfo = context.getVideoRecordingInfo();
const shouldPauseDuringWait = recordingInfo.enabled &&
recordingInfo.mode !== 'continuous' &&
const shouldPauseDuringWait = recordingInfo.enabled &&
recordingInfo.mode !== 'continuous' &&
!params.recordDuringWait;
if (shouldPauseDuringWait) {
@ -76,9 +76,9 @@ const wait = defineTool({
}
response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
if (params.recordDuringWait && recordingInfo.enabled) {
if (params.recordDuringWait && recordingInfo.enabled)
response.addResult(`🎥 Video recording continued during wait`);
}
response.setIncludeSnapshot();
},
});