devChromeServer.mjs•32 kB
#!/usr/bin/env node
import puppeteer from "puppeteer";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const DEFAULT_URL = process.env.MCP_DEV_URL ?? "http://localhost:5173/";
const CHROME_PATH = process.env.MCP_CHROME_PATH ?? process.env.PUPPETEER_EXECUTABLE_PATH;
const NAV_TIMEOUT = Number(process.env.MCP_NAV_TIMEOUT ?? "45000");
const WAIT_TIMEOUT = Number(process.env.MCP_WAIT_TIMEOUT ?? "45000");
const MAX_LOG_ENTRIES = 500;
const MAX_SCREENSHOTS = 8;
const state = {
consoleLogs: [],
screenshots: new Map(),
browser: null,
page: null,
listenersAttached: false,
grantedPermissions: new Set(),
};
const AVAILABLE_PERMISSIONS = new Map([
["status", "Read current page metadata and console logs"],
["navigate", "Navigate to arbitrary URLs"],
["reload", "Reload the current page"],
["screenshot", "Capture screenshots of the page"],
["console_clear", "Clear stored console log entries"],
["eval", "Execute custom JavaScript within the page"],
["wait_selector", "Wait for DOM elements matching a selector"],
["click", "Click elements within the page"],
["type", "Type text into elements"],
["scroll", "Scroll the page viewport"],
["set_viewport", "Adjust browser viewport dimensions"],
]);
function normalizePermission(name) {
return String(name ?? "")
.trim()
.toLowerCase();
}
function expandPermissionInputs(names) {
const normalized = new Set();
for (const raw of names ?? []) {
const value = normalizePermission(raw);
if (!value) {
continue;
}
if (value === "all" || value === "*") {
for (const key of AVAILABLE_PERMISSIONS.keys()) {
normalized.add(key);
}
continue;
}
if (!AVAILABLE_PERMISSIONS.has(value)) {
throw new Error(`Unknown permission: ${value}`);
}
normalized.add(value);
}
if (normalized.size === 0) {
throw new Error("No valid permissions provided");
}
return Array.from(normalized.values()).sort();
}
function hasPermission(name) {
return state.grantedPermissions.has(name);
}
function requirePermission(name) {
if (!hasPermission(name)) {
throw new Error(`Permission "${name}" not granted. Grant it via dev_grant_permissions.`);
}
}
function permissionStatusReport() {
const lines = [];
for (const [name, description] of AVAILABLE_PERMISSIONS) {
const marker = hasPermission(name) ? "granted" : "denied";
lines.push(`[${marker}] ${name} - ${description}`);
}
return lines.join("\\n");
}
function serializeForOutput(value) {
if (value === undefined) {
return "undefined";
}
if (value === null) {
return "null";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" && !Number.isFinite(value)) {
return value.toString();
}
if (typeof value === "function") {
return value.toString();
}
try {
return JSON.stringify(value, null, 2);
} catch (_error) {
return String(value);
}
}
const server = new Server(
{
name: "dev-chrome-monitor",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
},
);
function notifyConsoleUpdate() {
try {
server.notification({
method: "notifications/resources/updated",
params: { uri: "console://logs" },
});
} catch (error) {
// Swallow notifications issued before transport is ready
}
}
function notifyScreenshotUpdate() {
try {
server.notification({
method: "notifications/resources/list_changed",
params: {},
});
} catch (error) {
// Swallow notifications issued before transport is ready
}
}
function pushLog(kind, text) {
const entry = `${new Date().toISOString()} [${kind}] ${text}`;
state.consoleLogs.push(entry);
if (state.consoleLogs.length > MAX_LOG_ENTRIES) {
state.consoleLogs.splice(0, state.consoleLogs.length - MAX_LOG_ENTRIES);
}
notifyConsoleUpdate();
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForUrl(url, timeoutMs) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
try {
const response = await fetch(url, { method: "GET" });
if (response.ok || response.status === 404) {
return true;
}
} catch (error) {
// Ignore connection failures until timeout
}
await delay(1500);
}
return false;
}
function attachPageListeners(page) {
if (state.listenersAttached) {
return;
}
page.on("console", (message) => {
pushLog(`console:${message.type()}`, message.text());
});
page.on("pageerror", (error) => {
pushLog("pageerror", error.message ?? String(error));
});
page.on("requestfailed", (request) => {
const failure = request.failure();
pushLog(
"requestfailed",
`${request.method()} ${request.url()} ${failure ? failure.errorText : "unknown"}`,
);
});
page.on("response", (response) => {
if (response.status() >= 400) {
pushLog("response", `${response.status()} ${response.url()}`);
}
});
state.listenersAttached = true;
}
async function ensurePage() {
if (!state.browser) {
pushLog("info", "Launching Chrome instance for dev monitoring");
state.browser = await puppeteer.launch({
headless: false,
executablePath: CHROME_PATH,
defaultViewport: null,
args: [
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
"--disable-site-isolation-trials",
"--no-default-browser-check",
"--no-first-run",
],
});
const pages = await state.browser.pages();
state.page = pages.length > 0 ? pages[0] : await state.browser.newPage();
attachPageListeners(state.page);
}
if (!state.page || state.page.isClosed()) {
const pages = await state.browser.pages();
state.page = pages.length > 0 ? pages[0] : await state.browser.newPage();
attachPageListeners(state.page);
}
return state.page;
}
async function autoNavigate() {
const page = await ensurePage();
const targetUrl = DEFAULT_URL;
const ready = await waitForUrl(targetUrl, WAIT_TIMEOUT);
if (!ready) {
pushLog(
"warn",
`Dev server not reachable at ${targetUrl}. Start it and call dev_navigate when ready.`,
);
return;
}
try {
await page.goto(targetUrl, { waitUntil: "networkidle0", timeout: NAV_TIMEOUT });
pushLog("info", `Navigated to ${targetUrl}`);
} catch (error) {
pushLog("error", `Initial navigation failed: ${error.message ?? error}`);
}
}
function listResources() {
const resources = [];
if (hasPermission("status")) {
resources.push({
uri: "console://logs",
mimeType: "text/plain",
name: "Dev server console",
});
}
if (hasPermission("screenshot")) {
for (const name of state.screenshots.keys()) {
resources.push({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
});
}
}
return { resources };
}
function readResource(uri) {
if (uri === "console://logs") {
if (!hasPermission("status")) {
throw new Error('Permission "status" not granted. Grant it via dev_grant_permissions.');
}
return {
contents: [
{
uri,
mimeType: "text/plain",
text: state.consoleLogs.join("\n"),
},
],
};
}
if (uri.startsWith("screenshot://")) {
if (!hasPermission("screenshot")) {
throw new Error('Permission "screenshot" not granted. Grant it via dev_grant_permissions.');
}
const name = uri.split("://")[1];
const buffer = state.screenshots.get(name);
if (buffer) {
return {
contents: [
{
uri,
mimeType: "image/png",
blob: buffer,
},
],
};
}
}
throw new Error(`Resource not found: ${uri}`);
}
function registerHandlers() {
server.setRequestHandler(ListResourcesRequestSchema, async () => listResources());
server.setRequestHandler(ReadResourceRequestSchema, async (request) =>
readResource(request.params.uri.toString()),
);
async function recreatePage() {
try {
if (state.page && !state.page.isClosed()) {
await state.page.close();
}
} catch (error) {
}
state.page = null;
state.listenersAttached = false;
}
async function withPage(fn) {
const page = await ensurePage();
try {
return await fn(page);
} catch (error) {
const message = error && error.message ? error.message.toLowerCase() : String(error).toLowerCase();
if (message.includes("detached frame") || page.isClosed()) {
pushLog("warn", "Page context detached; recreating and retrying once");
await recreatePage();
const page2 = await ensurePage();
return await fn(page2);
}
throw error;
}
}
const tools = [
{
name: "dev_list_permissions",
description: "List available permissions and whether they are granted",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "dev_grant_permissions",
description: "Grant one or more permissions after confirmation",
inputSchema: {
type: "object",
properties: {
permissions: {
type: "array",
items: { type: "string" },
description: "Permissions to grant (use \"all\" or \"*\" for every permission)",
},
confirm: {
type: "boolean",
description: "Must be true to apply the changes",
},
},
required: ["permissions", "confirm"],
},
},
{
name: "dev_revoke_permissions",
description: "Revoke previously granted permissions",
inputSchema: {
type: "object",
properties: {
permissions: {
type: "array",
items: { type: "string" },
description: "Permissions to revoke (use \"all\" or \"*\" for every permission)",
},
confirm: {
type: "boolean",
description: "Must be true to apply the changes",
},
},
required: ["permissions", "confirm"],
},
},
{
name: "dev_status",
description: "Report current URL, title, and log counts (requires permission: status)",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "dev_navigate",
description: "Navigate the monitored page to the provided URL (requires permission: navigate)",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to open" },
},
required: ["url"],
},
},
{
name: "dev_reload",
description: "Reload the monitored page and wait for network idle (requires permission: reload)",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "dev_screenshot",
description: "Capture a screenshot of the monitored page (requires permission: screenshot)",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Identifier for the screenshot" },
fullPage: {
type: "boolean",
description: "Capture the full scrollable page when true",
},
},
required: [],
},
},
{
name: "dev_clear_console",
description: "Clear stored console log entries (requires permission: console_clear)",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "dev_eval",
description: "Evaluate JavaScript in the monitored page (requires permission: eval)",
inputSchema: {
type: "object",
properties: {
expression: { type: "string", description: "JavaScript expression to evaluate" },
awaitPromise: {
type: "boolean",
description: "Await returned promise values when true",
},
},
required: ["expression"],
},
},
{
name: "dev_wait_for_selector",
description: "Wait for a selector to appear (requires permission: wait_selector)",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector to wait for" },
timeout: {
type: "number",
description: "Timeout in milliseconds (defaults to MCP_WAIT_TIMEOUT)",
},
visible: {
type: "boolean",
description: "Require the element to be visible when true",
},
hidden: {
type: "boolean",
description: "Resolve when the element becomes hidden when true",
},
},
required: ["selector"],
},
},
{
name: "dev_click_selector",
description: "Click an element matching a selector (requires permission: click)",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector of element to click" },
button: {
type: "string",
enum: ["left", "middle", "right"],
description: "Mouse button to use",
},
clickCount: {
type: "number",
description: "Number of click repetitions",
},
delay: {
type: "number",
description: "Delay between mousedown and mouseup in milliseconds",
},
timeout: {
type: "number",
description: "Timeout in milliseconds to find the element",
},
},
required: ["selector"],
},
},
{
name: "dev_type_selector",
description: "Type text into an element (requires permission: type)",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector of input element" },
text: { type: "string", description: "Text to type" },
delay: {
type: "number",
description: "Delay between key presses in milliseconds",
},
clear: {
type: "boolean",
description: "Clear existing value before typing",
},
timeout: {
type: "number",
description: "Timeout in milliseconds to find the element",
},
},
required: ["selector", "text"],
},
},
{
name: "dev_scroll",
description: "Scroll the page viewport (requires permission: scroll)",
inputSchema: {
type: "object",
properties: {
x: {
type: "number",
description: "Horizontal scroll position in pixels",
},
y: {
type: "number",
description: "Vertical scroll position in pixels",
},
behavior: {
type: "string",
enum: ["auto", "smooth"],
description: "Scroll behavior",
},
},
required: [],
},
},
{
name: "dev_set_viewport",
description: "Adjust the browser viewport (requires permission: set_viewport)",
inputSchema: {
type: "object",
properties: {
width: { type: "number", description: "Viewport width in pixels" },
height: { type: "number", description: "Viewport height in pixels" },
deviceScaleFactor: {
type: "number",
description: "Device scale factor to apply",
},
},
required: ["width", "height"],
},
},
];
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = request.params.name;
const args = request.params.arguments ?? {};
try {
switch (tool) {
case "dev_list_permissions": {
const report = permissionStatusReport();
const granted = Array.from(state.grantedPermissions).sort();
const textResult = `${report}
Granted: ${granted.length ? granted.join(", ") : "(none)"}`;
return {
content: [{
type: "text",
text: textResult,
}],
isError: false,
};
}
case "dev_grant_permissions": {
const permissionsInput = args.permissions;
if (!Array.isArray(permissionsInput) || permissionsInput.length === 0) {
throw new Error('Argument "permissions" must be a non-empty array.');
}
if (args.confirm !== true) {
throw new Error('Set "confirm" to true to grant permissions.');
}
const permissions = expandPermissionInputs(permissionsInput);
const granted = [];
const already = [];
for (const name of permissions) {
if (hasPermission(name)) {
already.push(name);
} else {
state.grantedPermissions.add(name);
granted.push(name);
}
}
const message = [
granted.length ? `Granted: ${granted.join(", ")}` : null,
already.length ? `Already granted: ${already.join(", ")}` : null,
]
.filter(Boolean)
.join("\n") || "No changes";
return {
content: [{ type: "text", text: message }],
isError: false,
};
}
case "dev_revoke_permissions": {
const permissionsInput = args.permissions;
if (!Array.isArray(permissionsInput) || permissionsInput.length === 0) {
throw new Error('Argument "permissions" must be a non-empty array.');
}
if (args.confirm !== true) {
throw new Error('Set "confirm" to true to revoke permissions.');
}
const permissions = expandPermissionInputs(permissionsInput);
const revoked = [];
const missing = [];
for (const name of permissions) {
if (hasPermission(name)) {
state.grantedPermissions.delete(name);
revoked.push(name);
} else {
missing.push(name);
}
}
const message = [
revoked.length ? `Revoked: ${revoked.join(", ")}` : null,
missing.length ? `Not granted: ${missing.join(", ")}` : null,
]
.filter(Boolean)
.join("\n") || "No changes";
return {
content: [{ type: "text", text: message }],
isError: false,
};
}
case "dev_status": {
requirePermission("status");
const { title, url } = await withPage(async (page) => ({ title: await page.title(), url: page.url() }));
return {
content: [{
type: "text",
text: `URL: ${url}
Title: ${title}
Console entries: ${state.consoleLogs.length}
Screenshots stored: ${state.screenshots.size}`,
}],
isError: false,
};
}
case "dev_navigate": {
requirePermission("navigate");
if (!args.url || typeof args.url !== "string") {
throw new Error("Missing required argument: url");
}
await withPage(async (page) => page.goto(args.url, { waitUntil: "networkidle0", timeout: NAV_TIMEOUT }));
pushLog("info", `Navigated to ${args.url}`);
return {
content: [{ type: "text", text: `Navigated to ${args.url}` }],
isError: false,
};
}
case "dev_reload": {
requirePermission("reload");
await withPage(async (page) => page.reload({ waitUntil: "networkidle0", timeout: NAV_TIMEOUT }));
pushLog("info", "Reloaded monitored page");
return {
content: [{ type: "text", text: "Reloaded page" }],
isError: false,
};
}
case "dev_screenshot": {
requirePermission("screenshot");
const name = typeof args.name === "string" && args.name.trim().length > 0
? args.name.trim()
: `screenshot-${Date.now()}`;
const buffer = await withPage(async (page) => page.screenshot({
type: "png",
fullPage: Boolean(args.fullPage),
}));
if (state.screenshots.size >= MAX_SCREENSHOTS) {
const firstKey = state.screenshots.keys().next().value;
state.screenshots.delete(firstKey);
}
state.screenshots.set(name, buffer);
notifyScreenshotUpdate();
pushLog("info", `Captured screenshot ${name}`);
return {
content: [{ type: "text", text: `Captured screenshot ${name}` }],
isError: false,
};
}
case "dev_clear_console": {
requirePermission("console_clear");
state.consoleLogs.length = 0;
notifyConsoleUpdate();
return {
content: [{ type: "text", text: "Cleared console log buffer" }],
isError: false,
};
}
case "dev_eval": {
requirePermission("eval");
if (typeof args.expression !== "string" || !args.expression.trim()) {
throw new Error('Argument "expression" must be a non-empty string.');
}
const expression = args.expression.trim();
const awaitPromise = Boolean(args.awaitPromise);
const result = await withPage(async (page) =>
page.evaluate(
({ code, wait }) => {
const runner = new Function(`return (${code});`);
const value = runner();
if (wait && value && typeof value.then === "function") {
return value;
}
if (!wait && value && typeof value.then === "function") {
return "[Promise]";
}
return value;
},
{ code: expression, wait: awaitPromise },
),
);
return {
content: [{ type: "text", text: serializeForOutput(result) }],
isError: false,
};
}
case "dev_wait_for_selector": {
requirePermission("wait_selector");
if (typeof args.selector !== "string" || !args.selector.trim()) {
throw new Error('Argument "selector" must be a non-empty string.');
}
const selector = args.selector.trim();
const timeout = args.timeout !== undefined ? Number(args.timeout) : WAIT_TIMEOUT;
if (!Number.isFinite(timeout) || timeout <= 0) {
throw new Error('Argument "timeout" must be a positive number when provided.');
}
const visible = args.visible === undefined ? undefined : Boolean(args.visible);
const hidden = args.hidden === undefined ? undefined : Boolean(args.hidden);
if (visible && hidden) {
throw new Error('Arguments "visible" and "hidden" cannot both be true.');
}
await withPage(async (page) => {
const options = { timeout };
if (visible !== undefined) {
options.visible = visible;
}
if (hidden !== undefined) {
options.hidden = hidden;
}
await page.waitForSelector(selector, options);
});
return {
content: [{ type: "text", text: `Selector "${selector}" satisfied` }],
isError: false,
};
}
case "dev_click_selector": {
requirePermission("click");
if (typeof args.selector !== "string" || !args.selector.trim()) {
throw new Error('Argument "selector" must be a non-empty string.');
}
const selector = args.selector.trim();
const timeout = args.timeout !== undefined ? Number(args.timeout) : WAIT_TIMEOUT;
if (!Number.isFinite(timeout) || timeout <= 0) {
throw new Error('Argument "timeout" must be a positive number when provided.');
}
const button = typeof args.button === "string" ? args.button : "left";
if (!["left", "middle", "right"].includes(button)) {
throw new Error('Argument "button" must be one of: left, middle, right.');
}
const clickCount = args.clickCount !== undefined ? Number(args.clickCount) : 1;
if (!Number.isFinite(clickCount) || clickCount <= 0) {
throw new Error('Argument "clickCount" must be a positive number when provided.');
}
const delay = args.delay !== undefined ? Number(args.delay) : undefined;
if (delay !== undefined && (!Number.isFinite(delay) || delay < 0)) {
throw new Error('Argument "delay" must be a non-negative number when provided.');
}
await withPage(async (page) => {
await page.waitForSelector(selector, { timeout });
const clickOptions = {
button,
clickCount: Math.round(clickCount),
};
if (delay !== undefined) {
clickOptions.delay = delay;
}
await page.click(selector, clickOptions);
});
return {
content: [{ type: "text", text: `Clicked selector "${selector}"` }],
isError: false,
};
}
case "dev_type_selector": {
requirePermission("type");
if (typeof args.selector !== "string" || !args.selector.trim()) {
throw new Error('Argument "selector" must be a non-empty string.');
}
if (typeof args.text !== "string") {
throw new Error('Argument "text" must be a string.');
}
const selector = args.selector.trim();
const textValue = args.text;
const timeout = args.timeout !== undefined ? Number(args.timeout) : WAIT_TIMEOUT;
if (!Number.isFinite(timeout) || timeout <= 0) {
throw new Error('Argument "timeout" must be a positive number when provided.');
}
const delay = args.delay !== undefined ? Number(args.delay) : undefined;
if (delay !== undefined && (!Number.isFinite(delay) || delay < 0)) {
throw new Error('Argument "delay" must be a non-negative number when provided.');
}
const clear = Boolean(args.clear);
await withPage(async (page) => {
await page.waitForSelector(selector, { timeout });
if (clear) {
await page.$eval(
selector,
(element) => {
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
element.value = "";
} else {
element.textContent = "";
}
},
);
}
if (delay !== undefined) {
await page.type(selector, textValue, { delay });
} else {
await page.type(selector, textValue);
}
});
return {
content: [{ type: "text", text: `Typed into selector "${selector}"` }],
isError: false,
};
}
case "dev_scroll": {
requirePermission("scroll");
const hasX = args.x !== undefined;
const hasY = args.y !== undefined;
const x = hasX ? Number(args.x) : undefined;
const y = hasY ? Number(args.y) : undefined;
if (hasX && !Number.isFinite(x)) {
throw new Error('Argument "x" must be a finite number when provided.');
}
if (hasY && !Number.isFinite(y)) {
throw new Error('Argument "y" must be a finite number when provided.');
}
const behavior = typeof args.behavior === "string" ? args.behavior : "auto";
if (!["auto", "smooth"].includes(behavior)) {
throw new Error('Argument "behavior" must be "auto" or "smooth" when provided.');
}
await withPage(async (page) =>
page.evaluate(
({ left, top, scrollBehavior }) => {
const options = { behavior: scrollBehavior };
if (left !== undefined) {
options.left = left;
}
if (top !== undefined) {
options.top = top;
}
window.scrollTo(options);
},
{
left: hasX ? x : undefined,
top: hasY ? y : undefined,
scrollBehavior: behavior,
},
),
);
return {
content: [{ type: "text", text: "Scrolled page" }],
isError: false,
};
}
case "dev_set_viewport": {
requirePermission("set_viewport");
const width = Number(args.width);
const height = Number(args.height);
if (!Number.isFinite(width) || width <= 0) {
throw new Error('Argument "width" must be a positive number.');
}
if (!Number.isFinite(height) || height <= 0) {
throw new Error('Argument "height" must be a positive number.');
}
const dsfProvided = args.deviceScaleFactor !== undefined;
const deviceScaleFactor = dsfProvided ? Number(args.deviceScaleFactor) : undefined;
if (dsfProvided && (!Number.isFinite(deviceScaleFactor) || deviceScaleFactor <= 0)) {
throw new Error('Argument "deviceScaleFactor" must be a positive number when provided.');
}
await withPage(async (page) =>
page.setViewport({
width: Math.round(width),
height: Math.round(height),
deviceScaleFactor: dsfProvided ? deviceScaleFactor : undefined,
}),
);
const suffix = dsfProvided ? ` @${deviceScaleFactor}x` : "";
return {
content: [{ type: "text", text: `Viewport set to ${Math.round(width)}x${Math.round(height)}${suffix}` }],
isError: false,
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${tool}` }],
isError: true,
};
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
pushLog("error", `Tool ${tool} failed: ${message}`);
return {
content: [{ type: "text", text: message }],
isError: true,
};
}
});
}
async function shutdown() {
if (state.browser) {
try {
await state.browser.close();
} catch (error) {
// ignore shutdown errors
}
state.browser = null;
state.page = null;
state.listenersAttached = false;
}
}
async function main() {
registerHandlers();
process.stdin.on("close", shutdown);
process.on("SIGINT", async () => {
await shutdown();
process.exit(0);
});
process.on("SIGTERM", async () => {
await shutdown();
process.exit(0);
});
const transport = new StdioServerTransport();
await server.connect(transport);
await autoNavigate();
}
main().catch(async (error) => {
console.error("Failed to start dev chrome monitor server", error);
await shutdown();
process.exit(1);
});