chore(extension): support custom executablePath (#947)
Fixes https://github.com/microsoft/playwright-mcp/issues/941
This commit is contained in:
@@ -276,3 +276,32 @@ for (const [mode, startClientMethod] of [
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => {
|
||||
useShortConnectionTimeout(1000);
|
||||
|
||||
const executablePath = test.info().outputPath('echo.sh');
|
||||
await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 });
|
||||
|
||||
const { client } = await startClient({
|
||||
args: [`--extension`],
|
||||
config: {
|
||||
browser: {
|
||||
launchOptions: {
|
||||
executablePath,
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const navigateResponse = await client.callTool({
|
||||
name: 'browser_navigate',
|
||||
arguments: { url: server.HELLO_WORLD },
|
||||
timeout: 1000,
|
||||
});
|
||||
expect(await navigateResponse).toHaveResponse({
|
||||
result: expect.stringContaining('Extension connection timeout.'),
|
||||
isError: true,
|
||||
});
|
||||
expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?');
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ export class CDPRelayServer {
|
||||
private _wsHost: string;
|
||||
private _browserChannel: string;
|
||||
private _userDataDir?: string;
|
||||
private _executablePath?: string;
|
||||
private _cdpPath: string;
|
||||
private _extensionPath: string;
|
||||
private _wss: WebSocketServer;
|
||||
@@ -73,10 +74,11 @@ export class CDPRelayServer {
|
||||
private _nextSessionId: number = 1;
|
||||
private _extensionConnectionPromise!: ManualPromise<void>;
|
||||
|
||||
constructor(server: http.Server, browserChannel: string, userDataDir?: string) {
|
||||
constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) {
|
||||
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
|
||||
this._browserChannel = browserChannel;
|
||||
this._userDataDir = userDataDir;
|
||||
this._executablePath = executablePath;
|
||||
|
||||
const uuid = crypto.randomUUID();
|
||||
this._cdpPath = `/cdp/${uuid}`;
|
||||
@@ -125,12 +127,16 @@ export class CDPRelayServer {
|
||||
if (toolName)
|
||||
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
|
||||
const href = url.toString();
|
||||
const executableInfo = registry.findExecutable(this._browserChannel);
|
||||
if (!executableInfo)
|
||||
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
|
||||
const executablePath = executableInfo.executablePath();
|
||||
if (!executablePath)
|
||||
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
|
||||
|
||||
let executablePath = this._executablePath;
|
||||
if (!executablePath) {
|
||||
const executableInfo = registry.findExecutable(this._browserChannel);
|
||||
if (!executableInfo)
|
||||
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
|
||||
executablePath = executableInfo.executablePath();
|
||||
if (!executablePath)
|
||||
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
|
||||
}
|
||||
|
||||
const args: string[] = [];
|
||||
if (this._userDataDir)
|
||||
|
||||
@@ -26,10 +26,12 @@ const debugLogger = debug('pw:mcp:relay');
|
||||
export class ExtensionContextFactory implements BrowserContextFactory {
|
||||
private _browserChannel: string;
|
||||
private _userDataDir?: string;
|
||||
private _executablePath?: string;
|
||||
|
||||
constructor(browserChannel: string, userDataDir: string | undefined) {
|
||||
constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) {
|
||||
this._browserChannel = browserChannel;
|
||||
this._userDataDir = userDataDir;
|
||||
this._executablePath = executablePath;
|
||||
}
|
||||
|
||||
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
|
||||
@@ -55,7 +57,7 @@ export class ExtensionContextFactory implements BrowserContextFactory {
|
||||
httpServer.close();
|
||||
throw new Error(abortSignal.reason);
|
||||
}
|
||||
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
|
||||
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath);
|
||||
abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
|
||||
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
||||
return cdpRelayServer;
|
||||
|
||||
@@ -72,7 +72,7 @@ program
|
||||
|
||||
const config = await resolveCLIConfig(options);
|
||||
const browserContextFactory = contextFactory(config);
|
||||
const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
|
||||
const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath);
|
||||
|
||||
if (options.extension) {
|
||||
const serverBackendFactory: mcpServer.ServerBackendFactory = {
|
||||
|
||||
Reference in New Issue
Block a user