Skip to content

Commit 16a6881

Browse files
committed
fix: always allow tmpdir access in client roots
1 parent 7ee5e86 commit 16a6881

3 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/McpContext.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
*/
66

77
import fs from 'node:fs/promises';
8+
import os from 'node:os';
89
import path from 'node:path';
9-
import {fileURLToPath} from 'node:url';
10+
import {fileURLToPath, pathToFileURL} from 'node:url';
1011

1112
import type {TargetUniverse} from './DevtoolsUtils.js';
1213
import {UniverseManager} from './DevtoolsUtils.js';
@@ -158,7 +159,16 @@ export class McpContext implements Context {
158159
}
159160

160161
roots(): Root[] | undefined {
161-
return this.#roots;
162+
if (this.#roots === undefined) {
163+
return undefined;
164+
}
165+
return [
166+
...this.#roots,
167+
{
168+
uri: pathToFileURL(os.tmpdir()).href,
169+
name: 'temp',
170+
},
171+
];
162172
}
163173

164174
setRoots(roots: Root[] | undefined): void {

tests/McpContext.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,15 @@ describe('McpContext', () => {
220220
await withMcpContext(async (_response, context) => {
221221
const roots = [{uri: 'file:///test', name: 'test'}];
222222
context.setRoots(roots);
223-
assert.deepEqual(context.roots(), roots);
223+
const actualRoots = context.roots();
224+
assert.ok(
225+
actualRoots?.some(r => r.name === 'test'),
226+
'Should contain the set root',
227+
);
228+
assert.ok(
229+
actualRoots?.some(r => r.name === 'temp'),
230+
'Should contain the temp root',
231+
);
224232
});
225233
});
226234

tests/roots.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import os from 'node:os';
9+
import path from 'node:path';
10+
import {describe, it} from 'node:test';
11+
import {pathToFileURL} from 'node:url';
12+
13+
import {withMcpContext} from './utils.js';
14+
15+
describe('McpContext Roots', () => {
16+
it('should allow access to os.tmpdir() even if roots are empty', async () => {
17+
await withMcpContext(async (_response, context) => {
18+
context.setRoots([]);
19+
const tmpPath = path.join(os.tmpdir(), 'test-file.txt');
20+
// This should not throw
21+
context.validatePath(tmpPath);
22+
});
23+
});
24+
25+
it('should allow access to os.tmpdir() when other roots are set', async () => {
26+
await withMcpContext(async (_response, context) => {
27+
const otherRoot = path.resolve(
28+
os.tmpdir(),
29+
'..',
30+
'other_workspace_root_for_test',
31+
);
32+
context.setRoots([{uri: pathToFileURL(otherRoot).href, name: 'other'}]);
33+
34+
const tmpPath = path.join(os.tmpdir(), 'test-file.txt');
35+
// This should not throw
36+
context.validatePath(tmpPath);
37+
38+
// Other root should also be allowed
39+
context.validatePath(path.join(otherRoot, 'file.txt'));
40+
41+
// Outside should still be denied. Use a path that is definitely not a root or temp dir.
42+
// We use a sibling directory to the temp dir or home dir.
43+
const outsidePath = path.resolve(
44+
os.homedir(),
45+
'a_very_unlikely_path_name_12345',
46+
);
47+
assert.throws(() => context.validatePath(outsidePath), /Access denied/);
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)