fix: surface nested error causes and autoConnect hint on connection failure (#3111)

### Summary
1. Set `autoConnect = true` when connecting via `channel` in
`BrowserManager#connect()` so connection failures include the
`chrome://inspect/#remote-debugging` hint even when `userDataDir` is not
explicitly set.
2. Walk the nested `error.cause` chain in `ToolHandler` so the
underlying filesystem error (`Cause: EPERM: operation not permitted,
open '.../DevToolsActivePort'`) is surfaced alongside the Puppeteer
`DevToolsActivePort` error.
3. Document the macOS `Files & Folders > Google Chrome` TCC permission
requirement for `--autoConnect` in `docs/troubleshooting.md`.

### Verification
- `npm run build && npm run check-format`
- `node --import ./build/tests/setup.js --test
build/tests/ToolHandler.test.js`
- `node --import ./build/tests/setup.js --test
build/tests/browser.test.js`
This commit is contained in:
Yuri (IURII) Trukhin authored and GitHub committed 2026-10-21 13:01:14 +00:00
1 parent ece9bca577
commit d7f1e78037
5 files changed
+108 -3

No files matched your search

+6
View File
@@ -62,6 +62,12 @@ On macOS, Chrome launched by an MCP client application (such as Claude Desktop)
To resolve this, grant Bluetooth permission to the MCP client application in `System Settings > Privacy & Security > Bluetooth`. After granting permission, restart the client application and start a new MCP session.
### `--autoConnect` fails with `Could not find DevToolsActivePort` or `EPERM: operation not permitted` on macOS
On recent macOS versions, macOS blocks other applications from reading browser profile folders under `~/Library/Application Support/Google/Chrome` without showing a permission prompt (TCC `kTCCServiceSystemPolicyAppDataDetailed`). Because the MCP server inherits the permissions of the terminal or IDE host application that launched it, reading `DevToolsActivePort` fails with `EPERM: operation not permitted`.
To resolve this, grant the terminal or MCP client host application access to **Google Chrome** in `System Settings > Privacy & Security > Files & Folders`. The change applies immediately without restarting the terminal or MCP client.
### Remote debugging between virtual machine (VM) and host fails
When attempting to connect to Chrome running on a host machine from within a virtual machine (VM), Chrome may reject the connection due to 'Host' header validation. You can bypass this restriction by creating an SSH tunnel from the VM to the host. In the VM, run:
+1 -1
View File
@@ -377,8 +377,8 @@ export class BrowserManager {
} else if (browserURL) {
connectOptions.browserURL = browserURL;
} else if (channel || userDataDir) {
autoConnect = true;
if (userDataDir) {
autoConnect = true;
// TODO: re-expose this logic via Puppeteer.
const portPath = path.join(userDataDir, 'DevToolsActivePort');
try {
+15 -2
View File
@@ -529,8 +529,21 @@ export class ToolHandler {
} catch (err) {
logger?.(`${tool.name} error:`, err, err?.stack);
let errorText = err && 'message' in err ? err.message : String(err);
if ('cause' in err && err.cause) {
errorText += `\nCause: ${err.cause.message}`;
let cause: unknown =
err && typeof err === 'object' && 'cause' in err
? err.cause
: undefined;
const seenCauses = new Set<unknown>([err]);
while (
cause &&
typeof cause === 'object' &&
!seenCauses.has(cause) &&
'message' in cause &&
typeof cause.message === 'string'
) {
seenCauses.add(cause);
errorText += `\nCause: ${cause.message}`;
cause = 'cause' in cause ? cause.cause : undefined;
}
return errorResult(errorText);
} finally {
+55
View File
@@ -2358,4 +2358,59 @@ describe('ToolHandler', () => {
assert.deepStrictEqual(result.structuredContent, {message: 'done'});
});
});
it('includes nested error causes in error output', async () => {
const config = parseConfig();
const tool: ToolDefinition = {
name: 'failing_connect_tool',
description: 'A tool whose ensureBrowser() fails with nested causes',
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {},
blockedByDialog: false,
concurrency: 'exclusive',
verifyFilesSchema: {},
handler: async () => undefined,
};
const rootCause = Object.assign(
new Error(
"EPERM: operation not permitted, open '/Users/test/Library/Application Support/Google/Chrome/DevToolsActivePort'",
),
{code: 'EPERM'},
);
const puppeteerError = new Error(
'Could not find DevToolsActivePort for chrome at /Users/test/Library/Application Support/Google/Chrome/DevToolsActivePort',
{cause: rootCause},
);
const connectError = new Error(
'Could not connect to Chrome. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.',
{cause: puppeteerError},
);
const mockContext = createMockMcpContext();
mockContext.ensureBrowser.rejects(connectError);
const toolHandler = new ToolHandler(
tool,
config,
async () => mockContext,
sinon.spy(),
);
const result = await toolHandler.handle({});
assert.strictEqual(result.isError, true);
const text =
result.content[0].type === 'text' ? result.content[0].text : '';
assert.strictEqual(
text,
[
'Could not connect to Chrome. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.',
'Cause: Could not find DevToolsActivePort for chrome at /Users/test/Library/Application Support/Google/Chrome/DevToolsActivePort',
"Cause: EPERM: operation not permitted, open '/Users/test/Library/Application Support/Google/Chrome/DevToolsActivePort'",
].join('\n'),
);
});
});
+31
View File
@@ -654,6 +654,37 @@ describe('browser', () => {
sinon.assert.calledTwice(launchStub);
});
});
it('includes remote debugging hint and preserves cause when autoConnect fails without userDataDir', async () => {
const enoentError = Object.assign(
new Error(
"ENOENT: no such file or directory, open '/Users/test/Library/Application Support/Google/Chrome/DevToolsActivePort'",
),
{code: 'ENOENT'},
);
const puppeteerError = new Error(
'Could not find DevToolsActivePort for chrome at /Users/test/Library/Application Support/Google/Chrome/DevToolsActivePort',
{cause: enoentError},
);
sinon.stub(puppeteer, 'connect').rejects(puppeteerError);
const manager = new BrowserManager(
createMockServerConfig({
autoConnect: true,
channel: 'stable',
}),
);
await assert.rejects(manager.ensureBrowser(), err => {
assert.ok(err instanceof Error);
assert.strictEqual(
err.message,
'Could not connect to Chrome. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.',
);
assert.strictEqual(err.cause, puppeteerError);
return true;
});
});
});
describe('rootSandboxLaunchError', () => {