Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Real launches keep Chrome's sandbox, Safe Browsing, popup blocking and updates on (no `--no-sandbox`).
- `browser status` reports each browser's `engine`, and `engine` can now be `real` in launch results
and lock files. `profile list` shows a Fingerprint column.
- `port` on `launch` and `browser open` (`--port`, MCP `port`): a fixed DevTools port, so a client
configured ahead of time, such as Playwright MCP with `--cdp-endpoint http://127.0.0.1:<port>`,
can reach the profile's browser. A browser that is already running keeps its port.

### Changed

Expand Down
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ profile returns the running browser with `reused=true`. Close it with `browser c
| `--headless` | Run Chrome without a visible window. Default false (a real window is far less detectable). Set true only on machines with no display. |
| `--start-url <value>` | Absolute URL to open in the first tab, e.g. `"https://example.com"`. Omit for a blank tab. |
| `--detached` | Let Chrome outlive the process that opened it. Default false: the opener keeps the browser protected (per-tab fingerprint injection, authenticated proxy relay) and closes it on exit. Set true only for scripts that must exit immediately; then only flag-level protections remain and an authenticated proxy will not work. Kernel mode is the exception: its spoof is in the Chrome flags, so later tabs stay spoofed after the opener exits. |
| `--port <value>` | Fixed DevTools port, e.g. `9301`, so a client configured ahead of time can connect to `http://127.0.0.1:<port>` (Playwright MCP `--cdp-endpoint`). Omit for a free port. A browser that is already running keeps its port. |
| `--engine <auto\|kernel\|inject>` | Where the fingerprint is applied. `kernel` needs a fingerprint-chromium binary and spoofs inside Chromium with no JavaScript hooks. `inject` patches stock Chrome over CDP. `auto` (default) uses kernel when the executable contains `--fingerprint-platform`, otherwise inject. A real profile only accepts `auto`; `kernel` or `inject` fails with `INVALID_CONFIG`. |

Output: `{ profileId, wsEndpoint, port, pid, reused, detached, engine }`. `engine` is `kernel`,
Expand Down
1 change: 1 addition & 0 deletions docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ interface LaunchOptions {
timeout?: number;
detached?: boolean; // let Chrome outlive this process (reduced protection)
engine?: 'auto' | 'kernel' | 'inject'; // default auto; a real profile accepts only auto
port?: number; // fixed DevTools port; default a free one
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Tool names are the command names with `_` instead of the dot: `profile.list` bec
| `profile_update` | Change fields of an existing profile. Omitted fields are left untouched. | `idOrName`, `name?`, `proxy?` (null removes it), `timezone?`, `fingerprint?`, `language?`, `platform?`, `tags?`, `notes?` |
| `profile_delete` | Delete a profile and all of its browser data. Irreversible. | `idOrName`, `force?` |
| `profile_duplicate` | Copy a profile settings (proxy, timezone, fingerprint) into a new profile with a new id. | `idOrName`, `name?` |
| `browser_open` | Open Chrome with a stored profile and return its CDP wsEndpoint. | `idOrName`, `headless?`, `startUrl?`, `detached?`, `engine?` |
| `browser_open` | Open Chrome with a stored profile and return its CDP wsEndpoint. | `idOrName`, `headless?`, `startUrl?`, `detached?`, `engine?`, `port?` |
| `browser_launch` | Launch a throwaway Chrome with a random fingerprint and no saved profile. | `proxy?`, `headless?`, `randomFingerprint?` (default true), `detached?` |
| `browser_close` | Close the browser running for a profile, from any process. | `idOrName` |
| `browser_close_all` | Close every browser started from this storage path, including temporary sessions. | none |
Expand Down
12 changes: 12 additions & 0 deletions docs/playwright-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ stay in effect.
Both MCP servers can be registered at once: one to manage profiles, one to drive pages. The agent
calls `browser_open`, then starts or reconfigures Playwright MCP with the returned endpoint.

## A fixed port: configure Playwright MCP once

With `--port`, the endpoint is known before the browser starts, so Playwright MCP can sit in an
MCP config file instead of being started per session:

```bash
browser-profiles browser open acme-main --port 9301
npx -y @playwright/mcp@latest --cdp-endpoint http://127.0.0.1:9301
```

Pick one port per profile. A browser that is already running keeps the port it started with.

## From code

The same endpoint works with any CDP client.
Expand Down
3 changes: 2 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ profile_duplicate Copy a profile's settings into a new profile with a new id.
browser_open Open Chrome with a stored profile and return its CDP endpoint.
args: idOrName (required), headless? (default false), startUrl? (absolute URL),
detached? (default false; true = Chrome outlives the caller, reduced protection),
engine? ("auto" | "kernel" | "inject"; a real profile accepts only "auto")
engine? ("auto" | "kernel" | "inject"; a real profile accepts only "auto"),
port? (fixed DevTools port; a running browser keeps its own)
out: {profileId, wsEndpoint, port, pid, reused, detached, engine}
engine is "kernel", "inject", or "real" (Chrome's own fingerprint)
browser_launch Throwaway Chrome with a random fingerprint and no saved profile. Nothing persists.
Expand Down
11 changes: 11 additions & 0 deletions src/commands/browser-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ export const browserOpen = defineCommand({
.describe('Absolute URL to open in the first tab, e.g. "https://example.com". Omit for a blank tab.'),
detached: detachedField,
engine: engineField,
port: z
.number()
.int()
.min(1024)
.max(65535)
.optional()
.describe(
'Fixed DevTools port, e.g. 9301, so a client set up ahead of time can connect to ' +
'http://127.0.0.1:<port>. Omit for a free port. A browser that is already running keeps its port.'
),
}),
async run(ctx, input): Promise<Result<BrowserOpened>> {
const found = await resolveProfile(ctx, input.idOrName);
Expand All @@ -60,6 +70,7 @@ export const browserOpen = defineCommand({
headless: input.headless ?? false,
detached: input.detached ?? false,
engine: input.engine,
port: input.port,
args: input.startUrl ? [input.startUrl] : [],
});

Expand Down
14 changes: 12 additions & 2 deletions src/launcher/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { launchChrome } from './launch';
// Chrome and its DevTools socket are faked; everything between them is the real launcher.
const fake = vi.hoisted(() => ({
port: 0,
launches: [] as Array<{ chromeFlags: string[]; envVars?: Record<string, string> }>,
launches: [] as Array<{ chromeFlags: string[]; envVars?: Record<string, string>; port?: number }>,
cdpCalls: [] as string[],
}));

vi.mock('./deps', () => ({
loadChromeLauncher: async () => ({
launch: async (options: { chromeFlags: string[]; envVars?: Record<string, string> }) => {
launch: async (options: { chromeFlags: string[]; envVars?: Record<string, string>; port?: number }) => {
fake.launches.push(options);
return { port: fake.port, pid: 2_000_000_000, kill: async () => undefined };
},
Expand Down Expand Up @@ -121,3 +121,13 @@ describe('launchChrome with a generated profile', () => {
expect(fake.cdpCalls).toEqual(expect.arrayContaining(['Browser.getVersion', 'Target.setAutoAttach']));
});
});

describe('launchChrome DevTools port', () => {
it('asks Chrome for the fixed port, and for none by default', async () => {
launched = await launchChrome({ profile: realProfile, userDataDir: path.join(dir, 'data'), chromePath, port: 9301 });
await launched.close();
launched = await launch(realProfile);

expect(fake.launches.map((options) => options.port)).toEqual([9301, undefined]);
});
});
2 changes: 2 additions & 0 deletions src/launcher/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ async function launchClaimed(options: ChromeLaunchOptions, userDataDir: string):
args = [],
extensions = [],
detached = false,
port,
} = options;

// Safe here only because the claim above proved no browser of ours is running.
Expand Down Expand Up @@ -186,6 +187,7 @@ async function launchClaimed(options: ChromeLaunchOptions, userDataDir: string):
kernelFlags: identity.engine === 'kernel' ? buildKernelFlags(profile, identity.timezone) : undefined,
}),
...(startingUrl ? { startingUrl } : {}),
...(port ? { port } : {}),
userDataDir,
ignoreDefaultFlags: true,
// Our own SIGINT handlers must run: chrome-launcher's kills and exits first.
Expand Down
1 change: 1 addition & 0 deletions src/profile-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export class BrowserProfiles {
timeout: options?.timeout,
detached: options?.detached,
engine: options?.engine,
port: options?.port,
});

log.debug(`Launched profile ${profileId} on port ${result.port}`);
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ export interface LaunchOptions {
* A real profile only accepts `auto`; it then launches with no engine.
*/
engine?: FingerprintEngine;
/**
* Fixed DevTools port, so a client configured ahead of time (e.g. Playwright MCP
* `--cdp-endpoint http://127.0.0.1:<port>`) can reach the browser. Default: a free port.
*/
port?: number;
}

/**
Expand Down
Loading