Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `true`

- **`--profileDirectory`/ `--profile-directory`, `-profile-dir`**
Specify which Chrome profile to use by specifying its directory name (e.g., "Profile 1", "Default") inside a chrome user data directory. Only works with --autoConnect or when launching Chrome via the Chrome DevTools MCP server.
- **Type:** string

<!-- END AUTO GENERATED OPTIONS -->

Pass them via the `args` property in the JSON configuration. For example:
Expand Down
123 changes: 123 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import path from 'node:path';
import {logger} from './logger.js';
import type {
Browser,
BrowserContext,
ChromeReleaseChannel,
LaunchOptions,
Target,
Expand Down Expand Up @@ -43,13 +44,92 @@ function makeTargetFilter() {
};
}

//Extracts the profile directory name from a user data dir path.
function getProfileNameFromUserDataDir(userDataDir: string): string {
const normalized = userDataDir.replace(/\\/g, '/');
const parts = normalized.split('/');
return parts[parts.length - 1] || 'Default';
}

async function getBrowserContextForProfile(
browser: Browser,
profileDirectory?: string,
): Promise<BrowserContext> {
if (!profileDirectory) {
return browser.defaultBrowserContext();
}

try {
const contexts = browser.browserContexts();
logger(`Found ${contexts.length} browser context(s)`);

for (const context of contexts) {
let page;
try {
page = await context.newPage();

await page.goto('chrome://version', {
waitUntil: 'domcontentloaded',
timeout: 3_000,
});

const profilePath: string | null = await page.evaluate(() => {
const body = document.querySelector('body');
if (!body) return null;
const text = (body.innerText || '');
const match = text.match(/Profile Path:\s*(.+)/i);
return match ? match[1].trim() : null;
});

try {
await page.close();
} catch {
//ignore close errors
}

if (!profilePath) {
continue;
}

const actualProfile = getProfileNameFromUserDataDir(profilePath);
logger(`Probed context: profilePath=${profilePath} => profileName=${actualProfile}`);

if (actualProfile === profileDirectory) {
logger(`Matched profile directory "${profileDirectory}" to a browser context`);
return context;
}
} catch (error) {
logger('Error probing a browser context for profile: ', error);
try {
if (page && !page.isClosed()) {
await page.close();
}
} catch {
// ignore
}
}
}

logger(
`Profile directory "${profileDirectory}" specified. ` +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this PR does not add anything useful without the implementation for finding the browser context that matches the profileDirectory name. Passing --profile-directory to Chrome is already possible via the --chrome-arg param. Would you be up to figuring out how to find the correct browser context? it might right changes on the CDP and Puppeteer side.

`Using default browser context. Full profile support will be added in a future update.`,
);

return browser.defaultBrowserContext();
} catch (error) {
logger('Error getting browser contexts: ', error);
return browser.defaultBrowserContext();
}
}

export async function ensureBrowserConnected(options: {
browserURL?: string;
wsEndpoint?: string;
wsHeaders?: Record<string, string>;
devtools: boolean;
channel?: Channel;
userDataDir?: string;
profileDirectory?: string;
}) {
const {channel} = options;
if (browser?.connected) {
Expand Down Expand Up @@ -126,7 +206,13 @@ export async function ensureBrowserConnected(options: {
},
);
}

logger('Connected Puppeteer');

if (options.profileDirectory) {
await getBrowserContextForProfile(browser, options.profileDirectory);
logger(`Using browser context for profile: ${options.profileDirectory}`);
}
return browser;
}

Expand All @@ -145,6 +231,7 @@ interface McpLaunchOptions {
chromeArgs?: string[];
ignoreDefaultChromeArgs?: string[];
devtools: boolean;
profileDirectory?: string;
}

export async function launch(options: McpLaunchOptions): Promise<Browser> {
Expand All @@ -171,6 +258,12 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
...(options.chromeArgs ?? []),
'--hide-crash-restore-bubble',
];
if (options.profileDirectory) {
args.push(`--profile-directory=${options.profileDirectory}`);
logger(
`Launcing Chrome with profile directory: ${options.profileDirectory}`,
);
}
const ignoreDefaultArgs: LaunchOptions['ignoreDefaultArgs'] =
options.ignoreDefaultChromeArgs ?? false;

Expand Down Expand Up @@ -215,6 +308,36 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
contentHeight: options.viewport.height,
});
}

if (options.profileDirectory && userDataDir) {
try {
await new Promise(resolve => setTimeout(resolve, 500));

const portPath = path.join(userDataDir, 'DevToolsActivePort');
const fileContent = await fs.promises.readFile(portPath, 'utf8');
const lines = fileContent
.split('\n')
.map(line => line.trim())
.filter(line => line);

if (lines.length >= 2) {
const browserPath = lines[1];
const actualProfile = getProfileNameFromUserDataDir(browserPath);
const requestedProfile = options.profileDirectory;

if (actualProfile !== requestedProfile) {
logger(
`Warning: Requested profile "${requestedProfile}" but Chrome may be using profile "${actualProfile}". ` +
`This could happen if Chrome is managing profiles differently.`,
);
} else {
logger(`Successfully validated profile: ${actualProfile}`);
}
}
} catch (error) {
logger('Could not validate profile directory after launch: ', error);
}
}
return browser;
} catch (error) {
if (
Expand Down
15 changes: 15 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ export const cliOptions = {
default: true,
describe: 'Set to false to exclude tools related to network.',
},
profileDirectory: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably should be using https://pptr.dev/api/puppeteer.browser.browsercontexts to locate the browser context for the profile (currently the default browser context is used)

type: 'string',
description:
'Specify which Chrome profile to use by specifying its directory name (e.g., "Profile 1", "Default") inside a chrome user data directory. Only works with --autoConnect or when launching Chrome via the Chrome DevTools MCP server.',
alias: 'profile-dir',
conflicts: ['browserUrl', 'wsEndpoint'],
},
usageStatistics: {
type: 'boolean',
// Marked as `false` until the feature is ready to be enabled by default.
Expand Down Expand Up @@ -273,6 +280,14 @@ export function parseArguments(version: string, argv = process.argv) {
'$0 --auto-connect --channel=canary',
'Connect to a canary Chrome instance (Chrome 145+) running instead of launching a new instance',
],
[
'$0 --auto-connect --profile-directory="Profile 1"',
'Connect to Chrome using a specific profile (requires Chrome 145+)',
],
[
'$0 --channel=stable --profile-directory="Work Profile"',
'Launch stable Chrome with a specific profile',
],
]);

return yargsInstance
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async function getContext(): Promise<McpContext> {
channel: args.autoConnect ? (args.channel as Channel) : undefined,
userDataDir: args.userDataDir,
devtools,
profileDirectory: args.profileDirectory,
})
: await ensureBrowserLaunched({
headless: args.headless,
Expand All @@ -85,6 +86,7 @@ async function getContext(): Promise<McpContext> {
ignoreDefaultChromeArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools,
profileDirectory: args.profileDirectory,
});

if (context?.browser !== browser) {
Expand Down