Skip to content

Commit 8173f21

Browse files
authored
Merge branch 'main' into mcp-dialog-pt1
2 parents 128195b + def53dd commit 8173f21

19 files changed

Lines changed: 298 additions & 43 deletions

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@types/yargs": "^17.0.33",
6464
"@typescript-eslint/eslint-plugin": "^8.43.0",
6565
"@typescript-eslint/parser": "^8.43.0",
66-
"chrome-devtools-frontend": "1.0.1613625",
66+
"chrome-devtools-frontend": "1.0.1618066",
6767
"core-js": "3.49.0",
6868
"debug": "4.4.3",
6969
"eslint": "^9.35.0",

scripts/post-build.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ export const LOCAL_FETCH_PATTERN = './locales/@LOCALE@.json';`;
5252
);
5353
fs.mkdirSync(codeMirrorDir, {recursive: true});
5454
const codeMirrorFile = path.join(codeMirrorDir, 'codemirror.next.js');
55-
const codeMirrorContent = `export default {}`;
55+
const codeMirrorContent = `
56+
export default {};
57+
export const cssStreamParser = () => Promise.resolve({ startState: () => ({}) });
58+
export class StringStream { constructor() {} }
59+
export const css = { cssLanguage: { parser: { parse: () => ({ topNode: { getChild: () => null } }) } } };
60+
`;
5661
writeFile(codeMirrorFile, codeMirrorContent);
5762

5863
// Create root mock
@@ -61,7 +66,13 @@ export const LOCAL_FETCH_PATTERN = './locales/@LOCALE@.json';`;
6166
const runtimeFile = path.join(rootDir, 'Runtime.js');
6267
const runtimeContent = `
6368
export function getChromeVersion() { return ''; };
69+
export function getRemoteBase() { return null; };
6470
export const hostConfig = {};
71+
export const GdpProfilesEnterprisePolicyValue = {
72+
ENABLED: 0,
73+
ENABLED_WITHOUT_BADGES: 1,
74+
DISABLED: 2,
75+
};
6576
export const Runtime = {
6677
isDescriptorEnabled: () => true,
6778
queryParam: () => null,
@@ -94,6 +105,33 @@ export const ExperimentName = {
94105
`;
95106
writeFile(runtimeFile, runtimeContent);
96107

108+
// Copy missing CodeMirror .mjs files that tsc ignores due to .d.mts renames
109+
const codemirrorDir = path.join(
110+
BUILD_DIR,
111+
devtoolsThirdPartyPath,
112+
'codemirror',
113+
);
114+
const codemirrorSrcDir = path.join(
115+
process.cwd(),
116+
'node_modules',
117+
'chrome-devtools-frontend',
118+
'front_end',
119+
'third_party',
120+
'codemirror',
121+
);
122+
const filesToCopy = [
123+
'package/addon/runmode/runmode-standalone.mjs',
124+
'package/mode/css/css.mjs',
125+
'package/mode/javascript/javascript.mjs',
126+
'package/mode/xml/xml.mjs',
127+
];
128+
for (const file of filesToCopy) {
129+
const src = path.join(codemirrorSrcDir, file);
130+
const dest = path.join(codemirrorDir, file);
131+
fs.mkdirSync(path.dirname(dest), {recursive: true});
132+
fs.copyFileSync(src, dest);
133+
}
134+
97135
copyDevToolsDescriptionFiles();
98136
}
99137

src/McpContext.ts

Lines changed: 64 additions & 16 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';
@@ -42,7 +44,7 @@ import type {
4244
GeolocationOptions,
4345
ExtensionServiceWorker,
4446
} from './types.js';
45-
import {ensureExtension, saveTemporaryFile} from './utils/files.js';
47+
import {ensureExtension, getTempFilePath} from './utils/files.js';
4648
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
4749

4850
interface McpContextOptions {
@@ -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,37 @@ 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+
if (filePath === undefined) {
170+
return;
171+
}
172+
const roots = this.roots();
173+
if (roots === undefined) {
174+
return;
175+
}
176+
const absolutePath = path.resolve(filePath);
177+
for (const root of roots) {
178+
const rootPath = path.resolve(fileURLToPath(root.uri));
179+
if (
180+
absolutePath === rootPath ||
181+
absolutePath.startsWith(rootPath + path.sep)
182+
) {
183+
return;
184+
}
185+
}
186+
throw new Error(
187+
`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`,
188+
);
189+
}
190+
157191
resolveCdpRequestId(page: McpPage, cdpRequestId: string): number | undefined {
158192
if (!cdpRequestId) {
159193
this.logger('no network request');
@@ -643,13 +677,22 @@ export class McpContext implements Context {
643677
data: Uint8Array<ArrayBufferLike>,
644678
filename: string,
645679
): Promise<{filepath: string}> {
646-
return await saveTemporaryFile(data, filename);
680+
const filepath = await getTempFilePath(filename);
681+
this.validatePath(filepath);
682+
try {
683+
await fs.writeFile(filepath, data);
684+
} catch (err) {
685+
throw new Error('Could not save a file', {cause: err});
686+
}
687+
return {filepath};
647688
}
689+
648690
async saveFile(
649691
data: Uint8Array<ArrayBufferLike>,
650692
clientProvidedFilePath: string,
651693
extension: SupportedExtensions,
652694
): Promise<{filename: string}> {
695+
this.validatePath(clientProvidedFilePath);
653696
try {
654697
const filePath = ensureExtension(
655698
path.resolve(clientProvidedFilePath),
@@ -721,6 +764,7 @@ export class McpContext implements Context {
721764
}
722765

723766
async installExtension(extensionPath: string): Promise<string> {
767+
this.validatePath(extensionPath);
724768
const id = await this.browser.installExtension(extensionPath);
725769
return id;
726770
}
@@ -751,25 +795,29 @@ export class McpContext implements Context {
751795
async getHeapSnapshotAggregates(
752796
filePath: string,
753797
): Promise<Record<string, AggregatedInfoWithUid>> {
798+
this.validatePath(filePath);
754799
return await this.#heapSnapshotManager.getAggregates(filePath);
755800
}
756801

757802
async getHeapSnapshotStats(
758803
filePath: string,
759804
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics> {
805+
this.validatePath(filePath);
760806
return await this.#heapSnapshotManager.getStats(filePath);
761807
}
762808

763809
async getHeapSnapshotStaticData(
764810
filePath: string,
765811
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null> {
812+
this.validatePath(filePath);
766813
return await this.#heapSnapshotManager.getStaticData(filePath);
767814
}
768815

769816
async getHeapSnapshotNodesByUid(
770817
filePath: string,
771818
uid: number,
772819
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
820+
this.validatePath(filePath);
773821
return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
774822
}
775823
}

src/daemon/client.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import net from 'node:net';
1111
import {logger} from '../logger.js';
1212
import type {CallToolResult} from '../third_party/index.js';
1313
import {PipeTransport} from '../third_party/index.js';
14-
import {saveTemporaryFile} from '../utils/files.js';
14+
import {getTempFilePath} from '../utils/files.js';
1515

1616
import type {DaemonMessage, DaemonResponse} from './types.js';
1717
import {
@@ -179,7 +179,8 @@ export async function handleResponse(
179179
}
180180
const data = Buffer.from(imageData, 'base64');
181181
const name = crypto.randomUUID();
182-
const {filepath} = await saveTemporaryFile(data, `${name}${extension}`);
182+
const filepath = await getTempFilePath(`${name}${extension}`);
183+
fs.writeFileSync(filepath, data);
183184
chunks.push(`Saved to ${filepath}.`);
184185
} else {
185186
throw new Error('Not supported response content type');

src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
McpServer,
2222
type CallToolResult,
2323
SetLevelRequestSchema,
24+
ListRootsResultSchema,
25+
RootsListChangedNotificationSchema,
2426
} from './third_party/index.js';
2527
import {ToolCategory} from './tools/categories.js';
2628
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
@@ -57,11 +59,35 @@ export async function createMcpServer(
5759
return {};
5860
});
5961

62+
const updateRoots = async () => {
63+
if (!server.server.getClientCapabilities()?.roots) {
64+
return;
65+
}
66+
try {
67+
const roots = await server.server.request(
68+
{method: 'roots/list'},
69+
ListRootsResultSchema,
70+
);
71+
context?.setRoots(roots.roots);
72+
} catch (e) {
73+
logger('Failed to list roots', e);
74+
}
75+
};
76+
6077
server.server.oninitialized = () => {
6178
const clientName = server.server.getClientVersion()?.name;
6279
if (clientName) {
6380
clearcutLogger?.setClientName(clientName);
6481
}
82+
if (server.server.getClientCapabilities()?.roots) {
83+
void updateRoots();
84+
server.server.setNotificationHandler(
85+
RootsListChangedNotificationSchema,
86+
() => {
87+
void updateRoots();
88+
},
89+
);
90+
}
6591
};
6692

6793
let context: McpContext;
@@ -109,6 +135,7 @@ export async function createMcpServer(
109135
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
110136
performanceCrux: serverArgs.performanceCrux,
111137
});
138+
await updateRoots();
112139
}
113140
return context;
114141
}

src/third_party/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export {
3030
SetLevelRequestSchema,
3131
type ImageContent,
3232
type TextContent,
33+
type Root,
34+
ListRootsRequestSchema,
35+
RootsListChangedNotificationSchema,
36+
ListRootsResultSchema,
3337
} from '@modelcontextprotocol/sdk/types.js';
3438
export {z as zod} from 'zod';
3539
export {default as ajv} from 'ajv';

src/tools/ToolDefinition.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,10 @@ export type SupportedExtensions =
167167
| '.json.gz';
168168

169169
/**
170-
* Only add methods required by tools/*.
170+
* Only add methods used by tools/*.
171171
*/
172172
export type Context = Readonly<{
173+
validatePath(filePath?: string): void;
173174
isRunningPerformanceTrace(): boolean;
174175
setIsRunningPerformanceTrace(x: boolean): void;
175176
isCruxEnabled(): boolean;
@@ -244,6 +245,9 @@ export type Context = Readonly<{
244245
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
245246
}>;
246247

248+
/**
249+
* Only add methods used by tools/*.
250+
*/
247251
export type ContextPage = Readonly<{
248252
readonly pptrPage: Page;
249253
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;

src/tools/input.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,9 @@ export const uploadFile = definePageTool({
365365
filePath: zod.string().describe('The local path of the file to upload'),
366366
includeSnapshot: includeSnapshotSchema,
367367
},
368-
handler: async (request, response) => {
368+
handler: async (request, response, context) => {
369369
const {uid, filePath} = request.params;
370+
context.validatePath(filePath);
370371
const handle = (await request.page.getElementByUid(
371372
uid,
372373
)) as ElementHandle<HTMLInputElement>;

src/tools/lighthouse.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const lighthouseAudit = definePageTool({
5353
outputDirPath,
5454
} = request.params;
5555

56+
context.validatePath(outputDirPath);
57+
5658
const flags: Flags = {
5759
onlyCategories: categories,
5860
output: formats,

0 commit comments

Comments
 (0)