feat: add Chrome extension support with session-based isolation

- Add browser_install_extension, browser_list_extensions, browser_uninstall_extension tools
- Support session-based extension isolation between MCP clients
- Extensions loaded via --load-extension Chrome flags at browser startup
- Browser auto-restarts when extensions are added/removed
- Validation ensures extensions only work with Chromium browser
- Warning system for Chrome channel vs pure Chromium compatibility
- Extension management persists across page navigations within session
- Updated README with complete extension tool documentation

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ryan Malloy 2025-08-21 15:02:00 -06:00
parent d8202f6694
commit b3dbe55a9d
4 changed files with 335 additions and 20 deletions

View file

@ -48,6 +48,9 @@ export class Context {
readonly sessionId: string;
private _sessionStartTime: number;
// Chrome extension management
private _installedExtensions: Array<{ path: string; name: string; version?: string }> = [];
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory, environmentIntrospector?: EnvironmentIntrospector) {
this.tools = tools;
this.config = config;
@ -250,7 +253,7 @@ export class Context {
result = await this._createVideoEnabledContext();
} else {
// Use the standard browser context factory
result = await this._browserContextFactory.createContext(this.clientVersion!);
result = await this._browserContextFactory.createContext(this.clientVersion!, this._getExtensionPaths());
}
const { browserContext } = result;
await this._setupRequestInterception(browserContext);
@ -275,12 +278,24 @@ export class Context {
// Get environment-specific browser options
const envOptions = this._environmentIntrospector.getRecommendedBrowserOptions();
const browser = await browserType.launch({
const launchOptions = {
...this.config.browser.launchOptions,
...envOptions, // Include environment-detected options
handleSIGINT: false,
handleSIGTERM: false,
});
};
// Add Chrome extension support for Chromium
const extensionPaths = this._getExtensionPaths();
if (this.config.browser.browserName === 'chromium' && extensionPaths.length > 0) {
testDebug(`Loading ${extensionPaths.length} Chrome extensions in video context: ${extensionPaths.join(', ')}`);
launchOptions.args = [
...(launchOptions.args || []),
...extensionPaths.map(path => `--load-extension=${path}`)
];
}
const browser = await browserType.launch(launchOptions);
// Use environment-specific video directory if available
const videoConfig = envOptions.recordVideo ?
@ -451,4 +466,81 @@ export class Context {
return videoPaths;
}
// Chrome Extension Management
async installExtension(extensionPath: string, extensionName: string): Promise<void> {
if (this.config.browser.browserName !== 'chromium')
throw new Error('Chrome extensions are only supported with Chromium browser.');
// Check if extension is already installed
const existingExtension = this._installedExtensions.find(ext => ext.path === extensionPath);
if (existingExtension)
throw new Error(`Extension is already installed: ${extensionName} (${extensionPath})`);
// Read extension manifest to get version info
const fs = await import('fs');
const path = await import('path');
const manifestPath = path.join(extensionPath, 'manifest.json');
let version: string | undefined;
try {
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
const manifest = JSON.parse(manifestContent);
version = manifest.version;
} catch (error) {
testDebug('Could not read extension version:', error);
}
// Add to installed extensions list
this._installedExtensions.push({
path: extensionPath,
name: extensionName,
version
});
testDebug(`Installing Chrome extension: ${extensionName} from ${extensionPath}`);
// Restart browser with updated extension list
await this._restartBrowserWithExtensions();
}
getInstalledExtensions(): Array<{ path: string; name: string; version?: string }> {
return [...this._installedExtensions];
}
async uninstallExtension(extensionPath: string): Promise<{ path: string; name: string; version?: string } | null> {
const extensionIndex = this._installedExtensions.findIndex(ext => ext.path === extensionPath);
if (extensionIndex === -1)
return null;
const removedExtension = this._installedExtensions.splice(extensionIndex, 1)[0];
testDebug(`Uninstalling Chrome extension: ${removedExtension.name} from ${extensionPath}`);
// Restart browser with updated extension list
await this._restartBrowserWithExtensions();
return removedExtension;
}
private async _restartBrowserWithExtensions(): Promise<void> {
// Close existing browser context if open
if (this._browserContextPromise) {
const { close } = await this._browserContextPromise;
await close();
this._browserContextPromise = undefined;
}
// Clear all tabs as they will be recreated
this._tabs = [];
this._currentTab = undefined;
testDebug(`Restarting browser with ${this._installedExtensions.length} extensions`);
}
private _getExtensionPaths(): string[] {
return this._installedExtensions.map(ext => ext.path);
}
}