Skip to content

Commit 732267d

Browse files
authored
fix: detect multiple instances and throw (#12)
We should be checking for browser logs in Puppeteer instead.
1 parent dba8b3c commit 732267d

File tree

2 files changed

+95
-31
lines changed

2 files changed

+95
-31
lines changed

src/browser.ts

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,22 @@ async function ensureBrowserConnected(browserURL: string) {
5454
return browser;
5555
}
5656

57-
async function ensureBrowserLaunched(
58-
headless: boolean,
59-
isolated: boolean,
60-
customDevToolsPath?: string,
61-
executablePath?: string,
62-
channel?: Channel,
63-
): Promise<Browser> {
64-
if (browser?.connected) {
65-
return browser;
66-
}
57+
type McpLaunchOptions = {
58+
executablePath?: string;
59+
customDevTools?: string;
60+
channel?: Channel;
61+
userDataDir?: string;
62+
headless: boolean;
63+
isolated: boolean;
64+
};
65+
66+
export async function launch(options: McpLaunchOptions): Promise<Browser> {
67+
const {channel, executablePath, customDevTools, headless, isolated} = options;
6768
const profileDirName =
6869
channel && channel !== 'stable' ? `mcp-profile-${channel}` : 'mcp-profile';
6970

70-
let userDataDir: string | undefined;
71-
if (!isolated) {
71+
let userDataDir = options.userDataDir;
72+
if (!isolated && !userDataDir) {
7273
userDataDir = path.join(
7374
os.homedir(),
7475
'.cache',
@@ -85,8 +86,8 @@ async function ensureBrowserLaunched(
8586
'--no-first-run',
8687
'--hide-crash-restore-bubble',
8788
];
88-
if (customDevToolsPath) {
89-
args.push(`--custom-devtools-frontend=file://${customDevToolsPath}`);
89+
if (customDevTools) {
90+
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
9091
}
9192
let puppeterChannel: ChromeReleaseChannel | undefined;
9293
if (!executablePath) {
@@ -95,16 +96,45 @@ async function ensureBrowserLaunched(
9596
? (`chrome-${channel}` as ChromeReleaseChannel)
9697
: 'chrome';
9798
}
98-
browser = await puppeteer.launch({
99-
...connectOptions,
100-
channel: puppeterChannel,
101-
executablePath,
102-
defaultViewport: null,
103-
userDataDir,
104-
pipe: true,
105-
headless,
106-
args,
107-
});
99+
100+
try {
101+
return await puppeteer.launch({
102+
...connectOptions,
103+
channel: puppeterChannel,
104+
executablePath,
105+
defaultViewport: null,
106+
userDataDir,
107+
pipe: true,
108+
headless,
109+
args,
110+
});
111+
} catch (error) {
112+
// TODO: check browser logs for `Failed to create a ProcessSingleton for
113+
// your profile directory` instead.
114+
if (
115+
userDataDir &&
116+
(error as Error).message.includes(
117+
'(Target.setDiscoverTargets): Target closed',
118+
)
119+
) {
120+
throw new Error(
121+
`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`,
122+
{
123+
cause: error,
124+
},
125+
);
126+
}
127+
throw error;
128+
}
129+
}
130+
131+
async function ensureBrowserLaunched(
132+
options: McpLaunchOptions,
133+
): Promise<Browser> {
134+
if (browser?.connected) {
135+
return browser;
136+
}
137+
browser = await launch(options);
108138
return browser;
109139
}
110140

@@ -118,13 +148,7 @@ export async function resolveBrowser(options: {
118148
}) {
119149
const browser = options.browserUrl
120150
? await ensureBrowserConnected(options.browserUrl)
121-
: await ensureBrowserLaunched(
122-
options.headless,
123-
options.isolated,
124-
options.customDevTools,
125-
options.executablePath,
126-
options.channel,
127-
);
151+
: await ensureBrowserLaunched(options);
128152

129153
return browser;
130154
}

tests/browser.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
import {describe, it} from 'node:test';
7+
import assert from 'node:assert';
8+
import os from 'node:os';
9+
import path from 'node:path';
10+
import {launch} from '../src/browser.js';
11+
12+
describe('browser', () => {
13+
it('cannot launch multiple times with the same profile', async () => {
14+
const tmpDir = os.tmpdir();
15+
const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`);
16+
const browser1 = await launch({
17+
headless: true,
18+
isolated: false,
19+
userDataDir: folderPath,
20+
});
21+
try {
22+
try {
23+
const browser2 = await launch({
24+
headless: true,
25+
isolated: false,
26+
userDataDir: folderPath,
27+
});
28+
await browser2.close();
29+
assert.fail('not reached');
30+
} catch (err) {
31+
assert.strictEqual(
32+
err.message,
33+
`The browser is already running for ${folderPath}. Use --isolated to run multiple browser instances.`,
34+
);
35+
}
36+
} finally {
37+
await browser1.close();
38+
}
39+
});
40+
});

0 commit comments

Comments
 (0)