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
1 change: 0 additions & 1 deletion scripts/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ const nodeArgs = [
'--test-reporter',
(process.env['NODE_TEST_REPORTER'] ?? process.env['CI']) ? 'spec' : 'dot',
'--test-force-exit',
'--test-concurrency=1',
'--test',
'--test-timeout=120000',
...flags,
Expand Down
108 changes: 66 additions & 42 deletions src/bin/chrome-devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ await checkForUpdates(
'Run `npm install -g chrome-devtools-mcp@latest` and `chrome-devtools start` to update and restart the daemon.',
);

async function start(args: string[]) {
async function start(args: string[], sessionId: string) {
const combinedArgs = [...args, ...defaultArgs];
await startDaemon(combinedArgs);
await startDaemon(combinedArgs, sessionId);
logDisclaimers(parseArguments(VERSION, combinedArgs));
}

Expand Down Expand Up @@ -78,6 +78,12 @@ const y = yargs(hideBin(process.argv))
.usage(
`Run 'chrome-devtools <command> --help' for help on the specific command.`,
)
.option('sessionId', {
type: 'string',
description: 'Session ID for daemon scoping',
default: '',
hidden: true,
})
.demandCommand()
.version(VERSION)
.strict()
Expand All @@ -96,8 +102,8 @@ y.command(
)
.strict(),
async argv => {
if (isDaemonRunning()) {
await stopDaemon();
if (isDaemonRunning(argv.sessionId)) {
await stopDaemon(argv.sessionId);
}
// Defaults but we do not want to affect the yargs conflict resolution.
if (argv.isolated === undefined && argv.userDataDir === undefined) {
Expand All @@ -107,46 +113,60 @@ y.command(
argv.headless = true;
}
const args = serializeArgs(cliOptions, argv);
await start(args);
await start(args, argv.sessionId);
process.exit(0);
},
).strict(); // Re-enable strict validation for other commands; this is applied to the yargs instance itself

y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
if (isDaemonRunning()) {
console.log('chrome-devtools-mcp daemon is running.');
const response = await sendCommand({
method: 'status',
});
if (response.success) {
const data = JSON.parse(response.result) as {
pid: number | null;
socketPath: string;
startDate: string;
version: string;
args: string[];
};
console.log(
`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`,
y.command(
'status',
'Checks if chrome-devtools-mcp is running',
y => y,
async argv => {
if (isDaemonRunning(argv.sessionId)) {
console.log('chrome-devtools-mcp daemon is running.');
const response = await sendCommand(
{
method: 'status',
},
argv.sessionId,
);
console.log(`args=${JSON.stringify(data.args)}`);
if (response.success) {
const data = JSON.parse(response.result) as {
pid: number | null;
socketPath: string;
startDate: string;
version: string;
args: string[];
};
console.log(
`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`,
);
console.log(`args=${JSON.stringify(data.args)}`);
} else {
console.error('Error:', response.error);
process.exit(1);
}
} else {
console.error('Error:', response.error);
process.exit(1);
console.log('chrome-devtools-mcp daemon is not running.');
}
} else {
console.log('chrome-devtools-mcp daemon is not running.');
}
process.exit(0);
});
process.exit(0);
},
);

y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
if (!isDaemonRunning()) {
y.command(
'stop',
'Stop chrome-devtools-mcp if any',
y => y,
async argv => {
const sessionId = argv.sessionId as string;
if (!isDaemonRunning(sessionId)) {
process.exit(0);
}
await stopDaemon(sessionId);
process.exit(0);
}
await stopDaemon();
process.exit(0);
});
},
);

for (const [commandName, commandDef] of Object.entries(commands)) {
const args = commandDef.args;
Expand Down Expand Up @@ -213,9 +233,10 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
}
},
async argv => {
const sessionId = argv.sessionId as string;
try {
if (!isDaemonRunning()) {
await start([]);
if (!isDaemonRunning(sessionId)) {
await start([], sessionId);
}

const commandArgs: Record<string, unknown> = {};
Expand All @@ -225,11 +246,14 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
}
}

const response = await sendCommand({
method: 'invoke_tool',
tool: commandName,
args: commandArgs,
});
const response = await sendCommand(
{
method: 'invoke_tool',
tool: commandName,
args: commandArgs,
},
sessionId,
);

if (response.success) {
console.log(
Expand Down
19 changes: 10 additions & 9 deletions src/daemon/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ function waitForFile(filePath: string, removed = false) {
});
}

export async function startDaemon(mcpArgs: string[] = []) {
if (isDaemonRunning()) {
export async function startDaemon(mcpArgs: string[] = [], sessionId: string) {
if (isDaemonRunning(sessionId)) {
logger('Daemon is already running');
return;
}

const pidFilePath = getPidFilePath();
const pidFilePath = getPidFilePath(sessionId);

if (fs.existsSync(pidFilePath)) {
fs.unlinkSync(pidFilePath);
Expand All @@ -83,7 +83,7 @@ export async function startDaemon(mcpArgs: string[] = []) {
const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
detached: true,
stdio: 'ignore',
env: process.env,
env: {...process.env, CHROME_DEVTOOLS_MCP_SESSION_ID: sessionId},
cwd: process.cwd(),
windowsHide: true,
});
Expand All @@ -99,8 +99,9 @@ const SEND_COMMAND_TIMEOUT = 60_000; // ms
*/
export async function sendCommand(
command: DaemonMessage,
sessionId: string,
): Promise<DaemonResponse> {
const socketPath = getSocketPath();
const socketPath = getSocketPath(sessionId);

const socket = net.createConnection({
path: socketPath,
Expand Down Expand Up @@ -133,15 +134,15 @@ export async function sendCommand(
});
}

export async function stopDaemon() {
if (!isDaemonRunning()) {
export async function stopDaemon(sessionId: string) {
if (!isDaemonRunning(sessionId)) {
logger('Daemon is not running');
return;
}

const pidFilePath = getPidFilePath();
const pidFilePath = getPidFilePath(sessionId);

await sendCommand({method: 'stop'});
await sendCommand({method: 'stop'}, sessionId);

await waitForFile(pidFilePath, /*removed=*/ true);
}
Expand Down
10 changes: 5 additions & 5 deletions src/daemon/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ import {VERSION} from '../version.js';
import type {DaemonMessage} from './types.js';
import {
DAEMON_CLIENT_NAME,
getDaemonPid,
getPidFilePath,
getSocketPath,
INDEX_SCRIPT_PATH,
IS_WINDOWS,
isDaemonRunning,
} from './utils.js';

const pid = getDaemonPid();
if (isDaemonRunning(pid)) {
const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
logger(`Daemon sessionId: ${sessionId}`);
if (isDaemonRunning(sessionId)) {
logger('Another daemon process is running.');
process.exit(1);
}
const pidFilePath = getPidFilePath();
const pidFilePath = getPidFilePath(sessionId);
fs.mkdirSync(path.dirname(pidFilePath), {
recursive: true,
});
fs.writeFileSync(pidFilePath, process.pid.toString());
logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);

const socketPath = getSocketPath();
const socketPath = getSocketPath(sessionId);

const startDate = new Date();
const mcpServerArgs = process.argv.slice(2);
Expand Down
33 changes: 19 additions & 14 deletions src/daemon/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,56 +24,60 @@ const APP_NAME = 'chrome-devtools-mcp';
export const DAEMON_CLIENT_NAME = 'chrome-devtools-cli-daemon';

// Using these paths due to strict limits on the POSIX socket path length.
export function getSocketPath(): string {
export function getSocketPath(sessionId: string): string {
const uid = os.userInfo().uid;
const suffix = sessionId ? `-${sessionId}` : '';
const appName = APP_NAME + suffix;

if (IS_WINDOWS) {
// Windows uses Named Pipes, not file paths.
// This format is required for server.listen()
return path.join('\\\\.\\pipe', APP_NAME, 'server.sock');
return path.join('\\\\.\\pipe', appName, 'server.sock');
}

// 1. Try XDG_RUNTIME_DIR (Linux standard, sometimes macOS)
if (process.env.XDG_RUNTIME_DIR) {
return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME, 'server.sock');
return path.join(process.env.XDG_RUNTIME_DIR, appName, 'server.sock');
}

// 2. macOS/Unix Fallback: Use /tmp/
// We use /tmp/ because it is much shorter than ~/Library/Application Support/
// and keeps us well under the 104-character limit.
return path.join('/tmp', `${APP_NAME}-${uid}.sock`);
return path.join('/tmp', `${appName}-${uid}.sock`);
}

export function getRuntimeHome(): string {
export function getRuntimeHome(sessionId: string): string {
const platform = os.platform();
const uid = os.userInfo().uid;
const suffix = sessionId ? `-${sessionId}` : '';
const appName = APP_NAME + suffix;

// 1. Check for the modern Unix standard
if (process.env.XDG_RUNTIME_DIR) {
return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME);
return path.join(process.env.XDG_RUNTIME_DIR, appName);
}

// 2. Fallback for macOS and older Linux
if (platform === 'darwin' || platform === 'linux') {
// /tmp is cleared on boot, making it perfect for PIDs
return path.join('/tmp', `${APP_NAME}-${uid}`);
return path.join('/tmp', `${appName}-${uid}`);
}

// 3. Windows Fallback
return path.join(os.tmpdir(), APP_NAME);
return path.join(os.tmpdir(), appName);
}

export const IS_WINDOWS = os.platform() === 'win32';

export function getPidFilePath() {
const runtimeDir = getRuntimeHome();
export function getPidFilePath(sessionId: string) {
const runtimeDir = getRuntimeHome(sessionId);
return path.join(runtimeDir, 'daemon.pid');
}

export function getDaemonPid() {
export function getDaemonPid(sessionId: string) {
try {
const pidFile = getPidFilePath();
logger(`Daemon pid file ${pidFile}`);
const pidFile = getPidFilePath(sessionId);
logger(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
if (!fs.existsSync(pidFile)) {
return null;
}
Expand All @@ -89,7 +93,8 @@ export function getDaemonPid() {
}
}

export function isDaemonRunning(pid = getDaemonPid()): pid is number {
export function isDaemonRunning(sessionId: string): boolean {
const pid = getDaemonPid(sessionId);
if (pid) {
try {
process.kill(pid, 0); // Throws if process doesn't exist
Expand Down
Loading
Loading