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 _wsHost: string;
|
||||||
private _browserChannel: string;
|
private _browserChannel: string;
|
||||||
private _userDataDir?: string;
|
private _userDataDir?: string;
|
||||||
|
private _executablePath?: string;
|
||||||
private _cdpPath: string;
|
private _cdpPath: string;
|
||||||
private _extensionPath: string;
|
private _extensionPath: string;
|
||||||
private _wss: WebSocketServer;
|
private _wss: WebSocketServer;
|
||||||
@@ -73,10 +74,11 @@ export class CDPRelayServer {
|
|||||||
private _nextSessionId: number = 1;
|
private _nextSessionId: number = 1;
|
||||||
private _extensionConnectionPromise!: ManualPromise<void>;
|
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._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
|
||||||
this._browserChannel = browserChannel;
|
this._browserChannel = browserChannel;
|
||||||
this._userDataDir = userDataDir;
|
this._userDataDir = userDataDir;
|
||||||
|
this._executablePath = executablePath;
|
||||||
|
|
||||||
const uuid = crypto.randomUUID();
|
const uuid = crypto.randomUUID();
|
||||||
this._cdpPath = `/cdp/${uuid}`;
|
this._cdpPath = `/cdp/${uuid}`;
|
||||||
@@ -125,12 +127,16 @@ export class CDPRelayServer {
|
|||||||
if (toolName)
|
if (toolName)
|
||||||
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
|
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
|
||||||
const href = url.toString();
|
const href = url.toString();
|
||||||
const executableInfo = registry.findExecutable(this._browserChannel);
|
|
||||||
if (!executableInfo)
|
let executablePath = this._executablePath;
|
||||||
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
|
if (!executablePath) {
|
||||||
const executablePath = executableInfo.executablePath();
|
const executableInfo = registry.findExecutable(this._browserChannel);
|
||||||
if (!executablePath)
|
if (!executableInfo)
|
||||||
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
|
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[] = [];
|
const args: string[] = [];
|
||||||
if (this._userDataDir)
|
if (this._userDataDir)
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ const debugLogger = debug('pw:mcp:relay');
|
|||||||
export class ExtensionContextFactory implements BrowserContextFactory {
|
export class ExtensionContextFactory implements BrowserContextFactory {
|
||||||
private _browserChannel: string;
|
private _browserChannel: string;
|
||||||
private _userDataDir?: 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._browserChannel = browserChannel;
|
||||||
this._userDataDir = userDataDir;
|
this._userDataDir = userDataDir;
|
||||||
|
this._executablePath = executablePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
|
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();
|
httpServer.close();
|
||||||
throw new Error(abortSignal.reason);
|
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());
|
abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
|
||||||
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
||||||
return cdpRelayServer;
|
return cdpRelayServer;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ program
|
|||||||
|
|
||||||
const config = await resolveCLIConfig(options);
|
const config = await resolveCLIConfig(options);
|
||||||
const browserContextFactory = contextFactory(config);
|
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) {
|
if (options.extension) {
|
||||||
const serverBackendFactory: mcpServer.ServerBackendFactory = {
|
const serverBackendFactory: mcpServer.ServerBackendFactory = {
|
||||||
|
|||||||
Reference in New Issue
Block a user