fix: print the no-roots temp-directory warning under default flags (#2927)

The condition also matches when filesystemRoot is the same array as
DEFAULT_FILESYSTEM_ROOT, so a no-roots client under default flags gets
the existing temp-directory warning. Explicitly empty roots still warn.
A client that connects without the MCP roots capability is supposed to
get a one-time stderr warning that file-writing tools are limited to the
OS temp directory. With default flags that warning never appears, even
though a write outside the temp directory is still refused, so the only
signal is an Access denied error on the first such write.

Tests for this live in `tests/McpServer.test.ts`.

Fixes #2917

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
Matt Van HornandMatt Van Horn authored and GitHub committed 2026-10-19 08:53:09 +00:00
1 parent e342c6e21b
commit 0dd8ed979e
2 files changed
+76 -1

No files matched your search

+3 -1
View File
@@ -10,6 +10,7 @@ import {pathToFileURL} from 'node:url';
import {BrowserManager} from './BrowserManager.js';
import {type ParsedArguments} from './config/ConfigParser.js';
import {DEFAULT_FILESYSTEM_ROOT} from './config/mcp-options.js';
import {loadIssueDescriptions} from './devtools/issueDescriptions.js';
import {McpContext} from './McpContext.js';
import {HeapSnapshotManager} from './processors/HeapSnapshotManager.js';
@@ -136,7 +137,8 @@ export class McpServer {
);
} else if (
!this.#serverArgs.allowUnrestrictedPaths &&
(this.#serverArgs.filesystemRoot ?? []).length === 0
((this.#serverArgs.filesystemRoot ?? []).length === 0 ||
this.#serverArgs.filesystemRoot === DEFAULT_FILESYSTEM_ROOT)
) {
console.warn(
'[chrome-devtools-mcp] The connecting client did not negotiate the MCP roots ' +
+73
View File
@@ -23,6 +23,7 @@ import type {ToolCategory} from '../src/tools/categories.js';
import type {ToolDefinition} from '../src/tools/ToolDefinition.js';
import {createMockMcpContext, createMockPuppeteerBrowser} from './mocks.js';
import {createTempFile} from './utils.js';
describe('McpServer', () => {
afterEach(() => {
@@ -143,6 +144,78 @@ describe('McpServer', () => {
});
});
describe('oninitialized', () => {
const tempDirectoryWarning =
'[chrome-devtools-mcp] The connecting client did not negotiate the MCP roots ' +
'capability. Tools that read or write local files will be restricted to the OS temp directory. ' +
'To restore the previous unrestricted behavior, start the server with ' +
'--allow-unrestricted-paths.';
async function warnOnInitialized(
extraArgs: string[] = [],
clientHasRoots = false,
) {
const {server} = await createTestServer(extraArgs);
const protocol = server.server.server;
sinon
.stub(protocol, 'getClientCapabilities')
.returns(clientHasRoots ? {roots: {}} : {});
sinon.stub(protocol, 'getClientVersion').returns(undefined);
if (clientHasRoots) {
sinon.stub(protocol, 'request').resolves({roots: []});
}
const warnStub = sinon.stub(console, 'warn');
const oninitialized = protocol.oninitialized;
assert.ok(oninitialized);
oninitialized();
await Promise.resolve();
return warnStub;
}
it('warns when default flags meet a client without roots', async () => {
const warnStub = await warnOnInitialized();
sinon.assert.calledOnceWithExactly(warnStub, tempDirectoryWarning);
});
it('warns when filesystem roots are explicitly empty', async () => {
using configFile = createTempFile(
JSON.stringify({filesystemRoot: []}),
'cd4a.test.config.empty-filesystem-root.json',
);
const warnStub = await warnOnInitialized(['--config', configFile.path]);
sinon.assert.calledOnceWithExactly(warnStub, tempDirectoryWarning);
});
it('does not warn for an explicit workspace', async () => {
const warnStub = await warnOnInitialized([
'--workspace=/tmp/explicit-workspace',
]);
sinon.assert.notCalled(warnStub);
});
it('does not warn when unrestricted paths are allowed', async () => {
const warnStub = await warnOnInitialized(['--allow-unrestricted-paths']);
sinon.assert.notCalled(warnStub);
});
it('does not warn when started via the CLI', async () => {
const warnStub = await warnOnInitialized(['--viaCli']);
sinon.assert.notCalled(warnStub);
});
it('does not warn when the client negotiates roots', async () => {
const warnStub = await warnOnInitialized([], true);
sinon.assert.notCalled(warnStub);
});
});
describe('tool list', () => {
async function listToolNames(extraArgs: string[] = []): Promise<string[]> {
const {server} = await createTestServer(extraArgs);