feat: add MCP client identification system with debug toolbar and custom code injection

- Implement comprehensive debug toolbar showing project name, session ID, client info, and uptime
- Add Django-style draggable toolbar with terminal aesthetics for multi-client identification
- Support custom JavaScript/CSS injection into all pages with session persistence
- Auto-injection system hooks into page creation lifecycle for seamless operation
- LLM-safe HTML comment wrapping prevents confusion during automated testing
- 5 new MCP tools: enable_debug_toolbar, inject_custom_code, list_injections, disable_debug_toolbar, clear_injections
- Session-based configuration storage with auto-injection on new pages
- Solves multi-parallel MCP client identification problem for development workflows

Tools added:
- browser_enable_debug_toolbar: Configure project identification overlay
- browser_inject_custom_code: Add custom JS/CSS to all session pages
- browser_list_injections: View active injection configuration
- browser_disable_debug_toolbar: Remove debug toolbar
- browser_clear_injections: Clean up custom injections

Files modified:
- src/tools/codeInjection.ts: Complete injection system (547 lines)
- src/context.ts: Added injection config and auto-injection hooks
- src/tools.ts: Registered new tools in main array
- test-code-injection-simple.cjs: Validation test suite

Addresses issue: "I'm running many different 'mcp clients' in parallel on the same machine.
It's sometimes hard to figure out what client a playwright window belongs to."
This commit is contained in:
Ryan Malloy 2025-09-09 06:25:38 -06:00
parent efe1627c3f
commit b7ec4faf60
4 changed files with 770 additions and 0 deletions

View file

@ -27,6 +27,7 @@ import { ArtifactManagerRegistry } from './artifactManager.js';
import type { Tool } from './tools/tool.js';
import type { FullConfig } from './config.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type { InjectionConfig } from './tools/codeInjection.js';
const testDebug = debug('pw:mcp:test');
@ -65,6 +66,9 @@ export class Context {
private _lastSnapshotFingerprint: string | undefined;
private _lastPageState: { url: string; title: string } | undefined;
// Code injection for debug toolbar and custom scripts
injectionConfig: InjectionConfig | undefined;
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory, environmentIntrospector?: EnvironmentIntrospector) {
this.tools = tools;
this.config = config;
@ -200,6 +204,8 @@ export class Context {
testDebug('Request interceptor attached to new page');
}
// Auto-inject debug toolbar and custom code
void this._injectCodeIntoPage(page);
}
private _onPageClosed(tab: Tab) {
@ -1009,4 +1015,72 @@ export class Context {
(this.config as any).consoleOutputFile = updates.consoleOutputFile === '' ? undefined : updates.consoleOutputFile;
}
/**
* Auto-inject debug toolbar and custom code into a new page
*/
private async _injectCodeIntoPage(page: playwright.Page): Promise<void> {
if (!this.injectionConfig || !this.injectionConfig.enabled) {
return;
}
try {
// Import the injection functions (dynamic import to avoid circular deps)
const { generateDebugToolbarScript, wrapInjectedCode, generateInjectionScript } = await import('./tools/codeInjection.js');
// Inject debug toolbar if enabled
if (this.injectionConfig.debugToolbar.enabled) {
const toolbarScript = generateDebugToolbarScript(
this.injectionConfig.debugToolbar,
this.sessionId,
this.clientVersion,
this._sessionStartTime
);
// Add to page init script for future navigations
await page.addInitScript(toolbarScript);
// Execute immediately if page is already loaded
if (page.url() && page.url() !== 'about:blank') {
await page.evaluate(toolbarScript).catch(error => {
testDebug('Error executing debug toolbar script on existing page:', error);
});
}
testDebug(`Debug toolbar auto-injected into page: ${page.url()}`);
}
// Inject custom code
for (const injection of this.injectionConfig.customInjections) {
if (!injection.enabled || !injection.autoInject) {
continue;
}
try {
const wrappedCode = wrapInjectedCode(
injection,
this.sessionId,
this.injectionConfig.debugToolbar.projectName
);
const injectionScript = generateInjectionScript(wrappedCode);
// Add to page init script
await page.addInitScript(injectionScript);
// Execute immediately if page is already loaded
if (page.url() && page.url() !== 'about:blank') {
await page.evaluate(injectionScript).catch(error => {
testDebug(`Error executing custom injection "${injection.name}" on existing page:`, error);
});
}
testDebug(`Custom injection "${injection.name}" auto-injected into page: ${page.url()}`);
} catch (error) {
testDebug(`Error injecting custom code "${injection.name}":`, error);
}
}
} catch (error) {
testDebug('Error in code injection system:', error);
}
}
}