Skip to content

Commit c5f81e7

Browse files
committed
feat: support client roots feature
1 parent dbddb2e commit c5f81e7

16 files changed

Lines changed: 869 additions & 683 deletions

docs/tool-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,12 @@ so returned values have to be JSON-serializable.
320320
**Parameters:**
321321

322322
- **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page.
323-
Example without arguments: `() => {
323+
Example without arguments: `() => {
324324
return document.title
325325
}` or `async () => {
326326
return await fetch("example.com")
327327
}`.
328-
Example with arguments: `(el) => {
328+
Example with arguments: `(el) => {
329329
return el.innerText;
330330
}`
331331

src/McpContext.ts

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import fs from 'node:fs/promises';
88
import path from 'node:path';
9+
import {fileURLToPath} from 'node:url';
910

1011
import type {TargetUniverse} from './DevtoolsUtils.js';
1112
import {UniverseManager} from './DevtoolsUtils.js';
@@ -18,21 +19,22 @@ import {
1819
type ListenerMap,
1920
type UncaughtError,
2021
} from './PageCollector.js';
21-
import type {
22-
Browser,
23-
BrowserContext,
24-
ConsoleMessage,
25-
Debugger,
26-
HTTPRequest,
27-
Page,
28-
ScreenRecorder,
29-
Viewport,
30-
Target,
31-
Extension,
22+
import {
23+
Locator,
24+
PredefinedNetworkConditions,
25+
type Browser,
26+
type BrowserContext,
27+
type ConsoleMessage,
28+
type Debugger,
29+
type HTTPRequest,
30+
type Page,
31+
type ScreenRecorder,
32+
type Viewport,
33+
type Target,
34+
type Extension,
35+
type Root,
36+
type DevTools,
3237
} from './third_party/index.js';
33-
import type {DevTools} from './third_party/index.js';
34-
import {Locator} from './third_party/index.js';
35-
import {PredefinedNetworkConditions} from './third_party/index.js';
3638
import {listPages} from './tools/pages.js';
3739
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
3840
import type {Context, SupportedExtensions} from './tools/ToolDefinition.js';
@@ -90,6 +92,7 @@ export class McpContext implements Context {
9092
#locatorClass: typeof Locator;
9193
#options: McpContextOptions;
9294
#heapSnapshotManager = new HeapSnapshotManager();
95+
#roots: Root[] | undefined = undefined;
9396

9497
private constructor(
9598
browser: Browser,
@@ -154,6 +157,34 @@ export class McpContext implements Context {
154157
return context;
155158
}
156159

160+
roots(): Root[] | undefined {
161+
return this.#roots;
162+
}
163+
164+
setRoots(roots: Root[] | undefined): void {
165+
this.#roots = roots;
166+
}
167+
168+
validatePath(filePath: string): void {
169+
const roots = this.roots();
170+
if (roots === undefined) {
171+
return;
172+
}
173+
const absolutePath = path.resolve(filePath);
174+
for (const root of roots) {
175+
const rootPath = path.resolve(fileURLToPath(root.uri));
176+
if (
177+
absolutePath === rootPath ||
178+
absolutePath.startsWith(rootPath + path.sep)
179+
) {
180+
return;
181+
}
182+
}
183+
throw new Error(
184+
`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`,
185+
);
186+
}
187+
157188
resolveCdpRequestId(page: McpPage, cdpRequestId: string): number | undefined {
158189
if (!cdpRequestId) {
159190
this.logger('no network request');
@@ -643,13 +674,21 @@ export class McpContext implements Context {
643674
data: Uint8Array<ArrayBufferLike>,
644675
filename: string,
645676
): Promise<{filepath: string}> {
677+
if (
678+
filename.includes('/') ||
679+
filename.includes('\\') ||
680+
filename.includes('..')
681+
) {
682+
throw new Error(`Invalid filename: ${filename}`);
683+
}
646684
return await saveTemporaryFile(data, filename);
647685
}
648686
async saveFile(
649687
data: Uint8Array<ArrayBufferLike>,
650688
clientProvidedFilePath: string,
651689
extension: SupportedExtensions,
652690
): Promise<{filename: string}> {
691+
this.validatePath(clientProvidedFilePath);
653692
try {
654693
const filePath = ensureExtension(
655694
path.resolve(clientProvidedFilePath),
@@ -721,6 +760,7 @@ export class McpContext implements Context {
721760
}
722761

723762
async installExtension(extensionPath: string): Promise<string> {
763+
this.validatePath(extensionPath);
724764
const id = await this.browser.installExtension(extensionPath);
725765
return id;
726766
}
@@ -751,25 +791,29 @@ export class McpContext implements Context {
751791
async getHeapSnapshotAggregates(
752792
filePath: string,
753793
): Promise<Record<string, AggregatedInfoWithUid>> {
794+
this.validatePath(filePath);
754795
return await this.#heapSnapshotManager.getAggregates(filePath);
755796
}
756797

757798
async getHeapSnapshotStats(
758799
filePath: string,
759800
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics> {
801+
this.validatePath(filePath);
760802
return await this.#heapSnapshotManager.getStats(filePath);
761803
}
762804

763805
async getHeapSnapshotStaticData(
764806
filePath: string,
765807
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null> {
808+
this.validatePath(filePath);
766809
return await this.#heapSnapshotManager.getStaticData(filePath);
767810
}
768811

769812
async getHeapSnapshotNodesByUid(
770813
filePath: string,
771814
uid: number,
772815
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
816+
this.validatePath(filePath);
773817
return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
774818
}
775819
}

0 commit comments

Comments
 (0)