@narumitw/pi-chrome-devtools 0.52.3 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +62 -9
  2. package/dist/chunks/{browser-settings-menu-VWCNG4HB.js → browser-settings-menu-N5O65I7S.js} +81 -7
  3. package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
  4. package/dist/chunks/chunk-B7B63PWB.js +465 -0
  5. package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
  6. package/dist/chunks/{chunk-RH6KESL3.js → chunk-GVBXT3FB.js} +369 -514
  7. package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
  8. package/dist/chunks/chunk-RTECHAEZ.js +373 -0
  9. package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
  10. package/dist/chunks/{menu-PLD3S3EO.js → menu-UBHW6UP2.js} +72 -39
  11. package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
  12. package/dist/chunks/tools-V7OR5KJ3.js +945 -0
  13. package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
  14. package/dist/index.ts +158 -195
  15. package/dist/index.ts.map +4 -4
  16. package/package.json +4 -3
  17. package/src/browser-manager.ts +282 -109
  18. package/src/browser-settings-menu.ts +98 -4
  19. package/src/cdp-client.ts +329 -83
  20. package/src/chrome-devtools.ts +51 -11
  21. package/src/lazy-tools.ts +37 -4
  22. package/src/menu.ts +80 -39
  23. package/src/render.ts +12 -1
  24. package/src/runtime.ts +92 -0
  25. package/src/settings.ts +47 -10
  26. package/src/tool-names.ts +16 -1
  27. package/src/tool-selector.ts +67 -30
  28. package/src/tools.ts +86 -10
  29. package/src/webmcp/discovery.ts +235 -0
  30. package/src/webmcp/policy.ts +402 -0
  31. package/src/webmcp/protocol.ts +414 -0
  32. package/src/webmcp/tools.ts +190 -0
  33. package/dist/chunks/browser-settings-menu-VWCNG4HB.js.map +0 -7
  34. package/dist/chunks/chunk-RH6KESL3.js.map +0 -7
  35. package/dist/chunks/menu-PLD3S3EO.js.map +0 -7
package/src/settings.ts CHANGED
@@ -61,6 +61,7 @@ export interface ResolvedChromeDevToolsSettings {
61
61
  tools?: ChromeDevToolsToolName[];
62
62
  updatedAt?: number;
63
63
  browser: EffectiveBrowserSettings;
64
+ webmcp: { enabled: boolean };
64
65
  }
65
66
 
66
67
  export interface SettingsLoadOptions {
@@ -74,6 +75,7 @@ export type UserSettingsFileStatus =
74
75
 
75
76
  interface SettingsLoadBase {
76
77
  effectiveBrowser: EffectiveBrowserSettings;
78
+ effectiveWebMcpEnabled: boolean;
77
79
  paths: { user: string; project?: string };
78
80
  userFile: UserSettingsFileStatus;
79
81
  warnings: string[];
@@ -94,6 +96,7 @@ interface NormalizedSettingsDocument {
94
96
  tools?: ChromeDevToolsToolName[];
95
97
  updatedAt?: number;
96
98
  browser?: UserBrowserSettings;
99
+ webmcp?: { enabled: boolean };
97
100
  }
98
101
 
99
102
  interface SettingsDocumentResult {
@@ -165,7 +168,8 @@ export async function loadSettings(options: SettingsLoadOptions = {}): Promise<S
165
168
  const recognized =
166
169
  settings.tools !== undefined ||
167
170
  user.normalized?.browser !== undefined ||
168
- project.normalized?.browser !== undefined;
171
+ project.normalized?.browser !== undefined ||
172
+ user.normalized?.webmcp !== undefined;
169
173
  const invalidReasons = [user, project]
170
174
  .filter((result) => result.kind === "invalid")
171
175
  .map((result) => result.reason)
@@ -179,6 +183,7 @@ export async function loadSettings(options: SettingsLoadOptions = {}): Promise<S
179
183
  : { kind: user.kind };
180
184
  const base = {
181
185
  effectiveBrowser,
186
+ effectiveWebMcpEnabled: settings.webmcp.enabled,
182
187
  paths,
183
188
  userFile,
184
189
  warnings,
@@ -200,6 +205,7 @@ function resolveSettings(
200
205
  return {
201
206
  ...(user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {}),
202
207
  browser,
208
+ webmcp: { enabled: user?.webmcp?.enabled ?? false },
203
209
  };
204
210
  }
205
211
 
@@ -293,7 +299,7 @@ async function readSettingsDocument(
293
299
  return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
294
300
  }
295
301
 
296
- const scopeWarnings = projectMachineSettingsWarnings(parsed, scope, filePath);
302
+ const scopeWarnings = projectOwnedSettingsWarnings(parsed, scope, filePath);
297
303
  try {
298
304
  const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
299
305
  return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
@@ -307,19 +313,27 @@ async function readSettingsDocument(
307
313
  }
308
314
  }
309
315
 
310
- function projectMachineSettingsWarnings(
316
+ function projectOwnedSettingsWarnings(
311
317
  document: Record<string, unknown>,
312
318
  scope: "project" | "user",
313
319
  filePath: string,
314
320
  ) {
315
- if (scope !== "project" || !isRecord(document.browser)) return [];
316
- const browser = document.browser;
317
- return ["endpoint", "autoLaunch", "executablePath"]
318
- .filter((field) => browser[field] !== undefined)
319
- .map(
320
- (field) =>
321
- `Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`,
321
+ if (scope !== "project") return [];
322
+ const browser = isRecord(document.browser) ? document.browser : undefined;
323
+ const warnings = browser
324
+ ? ["endpoint", "autoLaunch", "executablePath"]
325
+ .filter((field) => browser[field] !== undefined)
326
+ .map(
327
+ (field) =>
328
+ `Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`,
329
+ )
330
+ : [];
331
+ if (document.webmcp !== undefined) {
332
+ warnings.push(
333
+ `Chrome DevTools project webmcp settings ignored in ${filePath}; only the user settings file may enable experimental WebMCP.`,
322
334
  );
335
+ }
336
+ return warnings;
323
337
  }
324
338
 
325
339
  async function normalizeSettingsDocument(
@@ -346,6 +360,13 @@ async function normalizeSettingsDocument(
346
360
  normalized.browser = await normalizeBrowserSection(document.browser, scope, filePath, cwd);
347
361
  }
348
362
 
363
+ if (scope === "user" && document.webmcp !== undefined) {
364
+ if (!isRecord(document.webmcp) || typeof document.webmcp.enabled !== "boolean") {
365
+ throw new Error("expected webmcp.enabled to be a boolean");
366
+ }
367
+ normalized.webmcp = { enabled: document.webmcp.enabled };
368
+ }
369
+
349
370
  return normalized;
350
371
  }
351
372
 
@@ -526,6 +547,22 @@ export function saveSettings(
526
547
  );
527
548
  }
528
549
 
550
+ export function saveWebMcpSettings(
551
+ enabled: boolean,
552
+ operations: Partial<SettingsFileOperations> = {},
553
+ ): Promise<void> {
554
+ return queueSettingsMutation(
555
+ (current) => ({
556
+ ...current,
557
+ webmcp: {
558
+ ...(isRecord(current.webmcp) ? current.webmcp : {}),
559
+ enabled,
560
+ },
561
+ }),
562
+ operations,
563
+ );
564
+ }
565
+
529
566
  export function saveBrowserSettings(
530
567
  patch: BrowserSettingsPatch,
531
568
  operations: Partial<SettingsFileOperations> = {},
package/src/tool-names.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const CHROME_DEVTOOLS_TOOL_NAMES = [
1
+ export const CORE_CHROME_DEVTOOLS_TOOL_NAMES = [
2
2
  "chrome_devtools_list_pages",
3
3
  "chrome_devtools_select_page",
4
4
  "chrome_devtools_navigate",
@@ -6,4 +6,19 @@ export const CHROME_DEVTOOLS_TOOL_NAMES = [
6
6
  "chrome_devtools_screenshot",
7
7
  ] as const;
8
8
 
9
+ export const WEBMCP_TOOL_NAMES = [
10
+ "chrome_devtools_webmcp_list_tools",
11
+ "chrome_devtools_webmcp_call_tool",
12
+ ] as const;
13
+
14
+ export const CHROME_DEVTOOLS_TOOL_NAMES = [
15
+ ...CORE_CHROME_DEVTOOLS_TOOL_NAMES,
16
+ ...WEBMCP_TOOL_NAMES,
17
+ ] as const;
18
+
9
19
  export type ChromeDevToolsToolName = (typeof CHROME_DEVTOOLS_TOOL_NAMES)[number];
20
+ export type WebMcpToolName = (typeof WEBMCP_TOOL_NAMES)[number];
21
+
22
+ export function isWebMcpToolName(value: string): value is WebMcpToolName {
23
+ return WEBMCP_TOOL_NAMES.includes(value as WebMcpToolName);
24
+ }
@@ -9,6 +9,7 @@ import {
9
9
  launchAttemptLines,
10
10
  launchHint,
11
11
  launchModeLabel,
12
+ managedBrowserForOwner,
12
13
  } from "./browser-manager.js";
13
14
  import {
14
15
  applyAvailableChromeDevtoolsTools,
@@ -16,9 +17,14 @@ import {
16
17
  CHROME_DEVTOOLS_LOAD_TOOL_NAME,
17
18
  chromeDevtoolsToolExposureMode,
18
19
  } from "./lazy-tools.js";
19
- import { state } from "./runtime.js";
20
+ import { invalidateWebMcpOperations, state, webMcpEnabled } from "./runtime.js";
20
21
  import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
21
- import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
22
+ import {
23
+ CHROME_DEVTOOLS_TOOL_NAMES,
24
+ type ChromeDevToolsToolName,
25
+ CORE_CHROME_DEVTOOLS_TOOL_NAMES,
26
+ isWebMcpToolName,
27
+ } from "./tool-names.js";
22
28
 
23
29
  type CommandContext = ExtensionCommandContext;
24
30
 
@@ -35,6 +41,7 @@ interface ToolStatusSummary {
35
41
  availableChromeToolCount: number;
36
42
  loadedChromeToolCount: number;
37
43
  activeNonChromeToolCount: number;
44
+ capabilityCount: number;
38
45
  }
39
46
 
40
47
  type ToolSelectionSaveResult = "saved" | "active-tools-changed" | "failed";
@@ -48,7 +55,7 @@ export async function updateChromeDevtoolsTools(
48
55
  const generation = state.sessionGeneration;
49
56
  const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
50
57
  if (result !== "saved" || generation !== state.sessionGeneration) return;
51
- const status = await buildToolStatusMessage(pi);
58
+ const status = await buildToolStatusMessage(pi, ctx.sessionManager);
52
59
  if (generation !== state.sessionGeneration) return;
53
60
  ctx.ui.notify(`Chrome DevTools tool catalog ${action}.\n\n${status}`, "info");
54
61
  }
@@ -109,6 +116,14 @@ async function transactSelectedToolsNow(
109
116
  const previousActiveTools = pi.getActiveTools();
110
117
  const previousAvailableTools = availableChromeDevtoolsTools(pi);
111
118
  try {
119
+ const previousWebMcpTools = previousAvailableTools.filter(isWebMcpToolName);
120
+ const selectedWebMcpTools = selectedTools.filter(isWebMcpToolName);
121
+ if (!arraysEqual(previousWebMcpTools, selectedWebMcpTools)) {
122
+ invalidateWebMcpOperations(
123
+ ctx.sessionManager,
124
+ "Chrome DevTools WebMCP gateway availability changed",
125
+ );
126
+ }
112
127
  applyChromeDevtoolsTools(pi, selectedTools);
113
128
  await persistSettings(selectedTools);
114
129
  return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
@@ -154,7 +169,7 @@ export function applyChromeDevtoolsTools(
154
169
  applyAvailableChromeDevtoolsTools(pi, selectedTools);
155
170
  }
156
171
 
157
- function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
172
+ function getToolStatusSummary(pi: ExtensionAPI, owner: object): ToolStatusSummary {
158
173
  const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
159
174
  const activeToolNames = new Set(pi.getActiveTools());
160
175
  const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
@@ -164,8 +179,9 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
164
179
  const activeNonChromeToolCount = Array.from(activeToolNames).filter(
165
180
  (name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
166
181
  ).length;
182
+ const capabilityCount = effectiveCatalog(owner).length;
167
183
  const availabilityStatus =
168
- availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
184
+ availableChromeToolCount === capabilityCount
169
185
  ? "enabled"
170
186
  : availableChromeToolCount === 0
171
187
  ? "disabled"
@@ -176,36 +192,39 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
176
192
  availableChromeToolCount,
177
193
  loadedChromeToolCount,
178
194
  activeNonChromeToolCount,
195
+ capabilityCount,
179
196
  };
180
197
  }
181
198
 
182
- export async function buildToolStatusMessage(pi: ExtensionAPI) {
183
- const summary = getToolStatusSummary(pi);
199
+ export async function buildToolStatusMessage(pi: ExtensionAPI, owner: object) {
200
+ const summary = getToolStatusSummary(pi, owner);
184
201
  const persistedSetting = await persistedSettingLabel();
185
202
  return sanitizeChromeDevtoolsDisplay(
186
203
  [
187
204
  `Chrome DevTools tools available: ${formatRuntimeStatus(summary)}`,
188
205
  `Tool exposure: ${chromeDevtoolsToolExposureMode(pi)}`,
189
- `Loaded capability tools this session: ${summary.loadedChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
206
+ `Loaded capability tools this session: ${summary.loadedChromeToolCount}/${summary.capabilityCount}`,
207
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · confirmation required for every call" : "disabled · experimental"}`,
190
208
  `Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
191
209
  `Persisted tool catalog: ${persistedSetting}`,
192
- ...browserSettingsStatusLines(),
210
+ ...browserSettingsStatusLines(owner),
193
211
  ...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
194
212
  `Other active tools preserved: ${summary.activeNonChromeToolCount}`,
195
- `Endpoint: ${devToolsEndpoint()}`,
213
+ `Endpoint: ${devToolsEndpoint(owner)}`,
196
214
  `Endpoint source: ${endpointSourceLabel()}`,
197
- `Launch mode: ${launchModeLabel()}`,
198
- ...launchAttemptLines(),
215
+ `Launch mode: ${launchModeLabel(owner)}`,
216
+ ...launchAttemptLines(owner),
199
217
  ].join("\n"),
200
218
  );
201
219
  }
202
220
 
203
- export function buildQuickstartMessage() {
204
- return buildSettingsSetupMessage();
221
+ export function buildQuickstartMessage(owner: object) {
222
+ return buildSettingsSetupMessage(owner);
205
223
  }
206
224
 
207
- export function buildBrowserStatusMessage() {
208
- const lifecycle = browserLifecycleState();
225
+ export function buildBrowserStatusMessage(owner?: object) {
226
+ const lifecycle = browserLifecycleState(owner);
227
+ const webMcpIsEnabled = owner ? webMcpEnabled(owner) : false;
209
228
  const browserState =
210
229
  lifecycle === "starting"
211
230
  ? "starting managed browser"
@@ -221,36 +240,46 @@ export function buildBrowserStatusMessage() {
221
240
  [
222
241
  `Browser: ${browserState}`,
223
242
  "Viewing this status does not probe the endpoint or launch Chrome.",
224
- `Endpoint: ${devToolsEndpoint()}`,
243
+ `Endpoint: ${devToolsEndpoint(owner)}`,
225
244
  `Endpoint source: ${endpointSourceLabel()}`,
226
- `Launch mode: ${launchModeLabel()}`,
245
+ `Launch mode: ${launchModeLabel(owner)}`,
227
246
  `Unpacked extensions: ${state.extensionPaths.length} (${state.extensionPathsSource})`,
247
+ `WebMCP: ${webMcpIsEnabled ? "enabled · experimental" : "disabled · experimental"}`,
248
+ ...(webMcpIsEnabled && !managedBrowserForOwner(owner)?.ready
249
+ ? [
250
+ "WebMCP warning: attached browser profiles may contain everyday authenticated sessions and sensitive state.",
251
+ ]
252
+ : []),
228
253
  ...(state.extensionPaths.length > 0
229
254
  ? ["Unpacked extensions execute trusted browser code in an isolated managed browser."]
230
255
  : []),
231
- ...launchAttemptLines(),
256
+ ...launchAttemptLines(owner),
232
257
  ...(needsRecovery ? [launchHint(), endpointConfigHint()] : []),
233
258
  ].join("\n"),
234
259
  );
235
260
  }
236
261
 
237
- export function buildSettingsSetupMessage() {
262
+ export function buildSettingsSetupMessage(owner: object) {
238
263
  return sanitizeChromeDevtoolsDisplay(
239
264
  [
240
- `Chrome DevTools endpoint: ${devToolsEndpoint()}`,
265
+ `Chrome DevTools endpoint: ${devToolsEndpoint(owner)}`,
241
266
  `Endpoint source: ${endpointSourceLabel()}`,
242
- `Launch mode: ${launchModeLabel()}`,
243
- ...browserSettingsStatusLines(),
267
+ `Launch mode: ${launchModeLabel(owner)}`,
268
+ ...browserSettingsStatusLines(owner),
244
269
  launchHint(),
245
270
  browserCandidateHint(),
246
- ...launchAttemptLines(),
271
+ ...launchAttemptLines(owner),
247
272
  endpointConfigHint(),
248
273
  ].join("\n"),
249
274
  );
250
275
  }
251
276
 
252
277
  export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_000) {
253
- const sanitized = Array.from(stripVTControlCharacters(value), (character) => {
278
+ const withoutBidi = stripVTControlCharacters(value).replace(
279
+ /[\u202a-\u202e\u2066-\u2069]/gu,
280
+ "�",
281
+ );
282
+ const sanitized = Array.from(withoutBidi, (character) => {
254
283
  const codePoint = character.codePointAt(0) ?? 0;
255
284
  const unsafeControl =
256
285
  (codePoint >= 0 && codePoint <= 8) ||
@@ -262,7 +291,7 @@ export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_
262
291
  return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}…`;
263
292
  }
264
293
 
265
- function browserSettingsStatusLines() {
294
+ function browserSettingsStatusLines(owner: object) {
266
295
  const extensionLines =
267
296
  state.extensionPaths.length > 0
268
297
  ? state.extensionPaths.map((extensionPath) => ` - ${extensionPath}`)
@@ -278,6 +307,7 @@ function browserSettingsStatusLines() {
278
307
  `Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
279
308
  `Unpacked extensions (${state.extensionPathsSource}):`,
280
309
  ...extensionLines,
310
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · every call requires confirmation" : "disabled · experimental"}`,
281
311
  "Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
282
312
  ...(state.extensionPaths.length > 0
283
313
  ? [
@@ -287,9 +317,10 @@ function browserSettingsStatusLines() {
287
317
  ];
288
318
  }
289
319
 
290
- export function buildCommandGuide() {
320
+ export function buildCommandGuide(owner: object) {
291
321
  return [
292
322
  "Chrome DevTools commands:",
323
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled" : "disabled"} · experimental · every page-provided call requires confirmation`,
293
324
  "/chrome-devtools — open this menu",
294
325
  "/chrome-devtools help — show command usage",
295
326
  "/chrome-devtools quickstart — show endpoint and launch help",
@@ -302,8 +333,8 @@ export function buildCommandGuide() {
302
333
  ].join("\n");
303
334
  }
304
335
 
305
- export function allChromeDevtoolsTools() {
306
- return [...CHROME_DEVTOOLS_TOOL_NAMES];
336
+ export function allChromeDevtoolsTools(owner: object) {
337
+ return effectiveCatalog(owner);
307
338
  }
308
339
 
309
340
  export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevToolsToolName>) {
@@ -311,7 +342,7 @@ export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevT
311
342
  }
312
343
 
313
344
  function formatRuntimeStatus(summary: ToolStatusSummary) {
314
- return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} available)`;
345
+ return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${summary.capabilityCount} available)`;
315
346
  }
316
347
 
317
348
  async function persistedSettingLabel() {
@@ -334,6 +365,12 @@ function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
334
365
  return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
335
366
  }
336
367
 
368
+ function effectiveCatalog(owner: object): ChromeDevToolsToolName[] {
369
+ return webMcpEnabled(owner)
370
+ ? [...CHROME_DEVTOOLS_TOOL_NAMES]
371
+ : [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
372
+ }
373
+
337
374
  async function persistSettings(selectedTools: readonly ChromeDevToolsToolName[]) {
338
375
  await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
339
376
  }
package/src/tools.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  import { renderScreenshotResult, renderTextResult, renderToolCall, withStatus } from "./render.js";
13
13
  import { state } from "./runtime.js";
14
14
  import { formatScreenshotText, saveScreenshot, throwIfAborted } from "./screenshot.js";
15
- import { CHROME_DEVTOOLS_TOOL_NAMES } from "./tool-names.js";
15
+ import { CHROME_DEVTOOLS_TOOL_NAMES, WEBMCP_TOOL_NAMES } from "./tool-names.js";
16
16
 
17
17
  export const listPagesTool = defineTool({
18
18
  name: CHROME_DEVTOOLS_TOOL_NAMES[0],
@@ -21,9 +21,9 @@ export const listPagesTool = defineTool({
21
21
  parameters: Type.Object({}),
22
22
  renderCall: renderToolCall("list pages"),
23
23
  renderResult: renderTextResult,
24
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
24
+ async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
25
25
  return withStatus(ctx, "list pages", async () => {
26
- const pages = await listPages();
26
+ const pages = await listPages({ sessionOwner: ctx.sessionManager, signal });
27
27
  return textResult(JSON.stringify(pages.map(formatPage), null, 2), { pages });
28
28
  });
29
29
  },
@@ -38,9 +38,9 @@ export const selectPageTool = defineTool({
38
38
  }),
39
39
  renderCall: renderToolCall("select page"),
40
40
  renderResult: renderTextResult,
41
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
41
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
42
42
  return withStatus(ctx, "select page", async () => {
43
- const page = await getPage(params.pageId);
43
+ const page = await getPage(params.pageId, { sessionOwner: ctx.sessionManager, signal });
44
44
  state.activePageId = page.id;
45
45
  return textResult(`Selected page ${page.id}: ${page.title}\n${page.url}`, {
46
46
  page: formatPage(page),
@@ -62,9 +62,12 @@ export const navigateTool = defineTool({
62
62
  }),
63
63
  renderCall: renderToolCall("navigate"),
64
64
  renderResult: renderTextResult,
65
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
65
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
66
66
  return withStatus(ctx, "navigate", async () => {
67
- const { created, page } = await resolvePageForNavigation(params.pageId);
67
+ const { created, page } = await resolvePageForNavigation(params.pageId, {
68
+ sessionOwner: ctx.sessionManager,
69
+ signal,
70
+ });
68
71
  const result = await withCdp(page, async (client) => {
69
72
  await client.send("Page.enable");
70
73
  return client.send("Page.navigate", { url: params.url });
@@ -96,9 +99,12 @@ export const evaluateTool = defineTool({
96
99
  }),
97
100
  renderCall: renderToolCall("evaluate"),
98
101
  renderResult: renderTextResult,
99
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
102
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
100
103
  return withStatus(ctx, "evaluate", async () => {
101
- const page = await resolvePage(params.pageId);
104
+ const page = await resolvePage(params.pageId, {
105
+ sessionOwner: ctx.sessionManager,
106
+ signal,
107
+ });
102
108
  const result = await withCdp(page, (client) =>
103
109
  client.send("Runtime.evaluate", {
104
110
  expression: params.expression,
@@ -113,6 +119,73 @@ export const evaluateTool = defineTool({
113
119
  },
114
120
  });
115
121
 
122
+ export const webMcpListToolsTool = defineTool({
123
+ name: WEBMCP_TOOL_NAMES[0],
124
+ label: "Chrome DevTools: List WebMCP Tools (Experimental)",
125
+ description:
126
+ "List bounded page-provided WebMCP tool descriptors from the selected Chrome page through the experimental CDP WebMCP domain. Definitions remain in this tool result and are never registered as dynamic Pi tools.",
127
+ parameters: Type.Object({
128
+ pageId: Type.Optional(
129
+ Type.String({
130
+ description: "Optional page id. Defaults to selected or first page.",
131
+ maxLength: 512,
132
+ }),
133
+ ),
134
+ }),
135
+ renderCall: renderToolCall("list WebMCP tools"),
136
+ renderResult: renderTextResult,
137
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
138
+ return withStatus(ctx, "list WebMCP tools", async () => {
139
+ const implementation = await import("./webmcp/tools.js");
140
+ return implementation.executeWebMcpListTool(params, signal, ctx);
141
+ });
142
+ },
143
+ });
144
+
145
+ export const webMcpCallTool = defineTool({
146
+ name: WEBMCP_TOOL_NAMES[1],
147
+ label: "Chrome DevTools: Call WebMCP Tool (Experimental)",
148
+ description:
149
+ "Call one previously listed page-provided WebMCP tool after exact page, document, frame, origin, schema, annotation, and session revalidation. Every call requires observable user confirmation and output is bounded to Pi's 50 KB or 2,000-line limit.",
150
+ parameters: Type.Object({
151
+ sessionGeneration: Type.String({
152
+ description: "Session generation token returned by chrome_devtools_webmcp_list_tools.",
153
+ maxLength: 100,
154
+ }),
155
+ pageId: Type.String({ description: "Page id returned by the list tool.", maxLength: 512 }),
156
+ documentId: Type.String({
157
+ description: "Document loader id returned by the list tool.",
158
+ maxLength: 512,
159
+ }),
160
+ frameId: Type.String({ description: "Frame id returned by the list tool.", maxLength: 512 }),
161
+ frameOrigin: Type.String({
162
+ description: "Exact frame origin returned by the list tool.",
163
+ maxLength: 2_048,
164
+ }),
165
+ toolName: Type.String({
166
+ description: "Exact tool name returned by the list tool.",
167
+ maxLength: 512,
168
+ }),
169
+ schemaDigest: Type.String({
170
+ description: "Schema and annotation digest returned by the list tool.",
171
+ minLength: 64,
172
+ maxLength: 64,
173
+ pattern: "^[a-f0-9]{64}$",
174
+ }),
175
+ input: Type.Record(Type.String(), Type.Unknown(), {
176
+ description: "JSON object matching the page-provided input schema.",
177
+ }),
178
+ }),
179
+ renderCall: renderToolCall("call WebMCP tool"),
180
+ renderResult: renderTextResult,
181
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
182
+ return withStatus(ctx, "call WebMCP tool", async () => {
183
+ const implementation = await import("./webmcp/tools.js");
184
+ return implementation.executeWebMcpCallTool(params, signal, ctx);
185
+ });
186
+ },
187
+ });
188
+
116
189
  export const screenshotTool = defineTool({
117
190
  name: CHROME_DEVTOOLS_TOOL_NAMES[4],
118
191
  label: "Chrome DevTools: Screenshot",
@@ -135,7 +208,10 @@ export const screenshotTool = defineTool({
135
208
  renderResult: renderScreenshotResult,
136
209
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
137
210
  return withStatus(ctx, "screenshot", async () => {
138
- const page = await resolvePage(params.pageId);
211
+ const page = await resolvePage(params.pageId, {
212
+ sessionOwner: ctx.sessionManager,
213
+ signal,
214
+ });
139
215
  const result = await withCdp(page, async (client) => {
140
216
  throwIfAborted(signal);
141
217
  await client.send("Page.enable");