@narumitw/pi-chrome-devtools 0.52.3 → 0.53.1

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-REIU7C3Q.js} +83 -7
  3. package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +7 -0
  4. package/dist/chunks/chunk-GTTYS5GT.js +384 -0
  5. package/dist/chunks/chunk-GTTYS5GT.js.map +7 -0
  6. package/dist/chunks/{chunk-RH6KESL3.js → chunk-SRT3UUIP.js} +473 -574
  7. package/dist/chunks/chunk-SRT3UUIP.js.map +7 -0
  8. package/dist/chunks/chunk-YJVYJ2L2.js +469 -0
  9. package/dist/chunks/chunk-YJVYJ2L2.js.map +7 -0
  10. package/dist/chunks/{menu-PLD3S3EO.js → menu-H5HMVGHI.js} +72 -39
  11. package/dist/chunks/menu-H5HMVGHI.js.map +7 -0
  12. package/dist/chunks/tools-6P7EUX37.js +964 -0
  13. package/dist/chunks/tools-6P7EUX37.js.map +7 -0
  14. package/dist/index.ts +166 -199
  15. package/dist/index.ts.map +4 -4
  16. package/package.json +4 -3
  17. package/src/browser-manager.ts +408 -169
  18. package/src/browser-settings-menu.ts +100 -5
  19. package/src/cdp-client.ts +347 -90
  20. package/src/chrome-devtools.ts +58 -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 +85 -44
  28. package/src/tools.ts +91 -15
  29. package/src/webmcp/discovery.ts +235 -0
  30. package/src/webmcp/policy.ts +409 -0
  31. package/src/webmcp/protocol.ts +426 -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
+ }
@@ -3,12 +3,14 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
3
3
  import {
4
4
  browserCandidateHint,
5
5
  browserLifecycleState,
6
+ browserSettingsForOwner,
6
7
  devToolsEndpoint,
7
8
  endpointConfigHint,
8
9
  endpointSourceLabel,
9
10
  launchAttemptLines,
10
11
  launchHint,
11
12
  launchModeLabel,
13
+ managedBrowserForOwner,
12
14
  } from "./browser-manager.js";
13
15
  import {
14
16
  applyAvailableChromeDevtoolsTools,
@@ -16,9 +18,14 @@ import {
16
18
  CHROME_DEVTOOLS_LOAD_TOOL_NAME,
17
19
  chromeDevtoolsToolExposureMode,
18
20
  } from "./lazy-tools.js";
19
- import { state } from "./runtime.js";
21
+ import { invalidateWebMcpOperations, state, webMcpEnabled } from "./runtime.js";
20
22
  import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
21
- import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
23
+ import {
24
+ CHROME_DEVTOOLS_TOOL_NAMES,
25
+ type ChromeDevToolsToolName,
26
+ CORE_CHROME_DEVTOOLS_TOOL_NAMES,
27
+ isWebMcpToolName,
28
+ } from "./tool-names.js";
22
29
 
23
30
  type CommandContext = ExtensionCommandContext;
24
31
 
@@ -35,6 +42,7 @@ interface ToolStatusSummary {
35
42
  availableChromeToolCount: number;
36
43
  loadedChromeToolCount: number;
37
44
  activeNonChromeToolCount: number;
45
+ capabilityCount: number;
38
46
  }
39
47
 
40
48
  type ToolSelectionSaveResult = "saved" | "active-tools-changed" | "failed";
@@ -48,7 +56,7 @@ export async function updateChromeDevtoolsTools(
48
56
  const generation = state.sessionGeneration;
49
57
  const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
50
58
  if (result !== "saved" || generation !== state.sessionGeneration) return;
51
- const status = await buildToolStatusMessage(pi);
59
+ const status = await buildToolStatusMessage(pi, ctx.sessionManager);
52
60
  if (generation !== state.sessionGeneration) return;
53
61
  ctx.ui.notify(`Chrome DevTools tool catalog ${action}.\n\n${status}`, "info");
54
62
  }
@@ -109,6 +117,14 @@ async function transactSelectedToolsNow(
109
117
  const previousActiveTools = pi.getActiveTools();
110
118
  const previousAvailableTools = availableChromeDevtoolsTools(pi);
111
119
  try {
120
+ const previousWebMcpTools = previousAvailableTools.filter(isWebMcpToolName);
121
+ const selectedWebMcpTools = selectedTools.filter(isWebMcpToolName);
122
+ if (!arraysEqual(previousWebMcpTools, selectedWebMcpTools)) {
123
+ invalidateWebMcpOperations(
124
+ ctx.sessionManager,
125
+ "Chrome DevTools WebMCP gateway availability changed",
126
+ );
127
+ }
112
128
  applyChromeDevtoolsTools(pi, selectedTools);
113
129
  await persistSettings(selectedTools);
114
130
  return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
@@ -154,7 +170,7 @@ export function applyChromeDevtoolsTools(
154
170
  applyAvailableChromeDevtoolsTools(pi, selectedTools);
155
171
  }
156
172
 
157
- function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
173
+ function getToolStatusSummary(pi: ExtensionAPI, owner: object): ToolStatusSummary {
158
174
  const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
159
175
  const activeToolNames = new Set(pi.getActiveTools());
160
176
  const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
@@ -164,8 +180,9 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
164
180
  const activeNonChromeToolCount = Array.from(activeToolNames).filter(
165
181
  (name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
166
182
  ).length;
183
+ const capabilityCount = effectiveCatalog(owner).length;
167
184
  const availabilityStatus =
168
- availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
185
+ availableChromeToolCount === capabilityCount
169
186
  ? "enabled"
170
187
  : availableChromeToolCount === 0
171
188
  ? "disabled"
@@ -176,36 +193,40 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
176
193
  availableChromeToolCount,
177
194
  loadedChromeToolCount,
178
195
  activeNonChromeToolCount,
196
+ capabilityCount,
179
197
  };
180
198
  }
181
199
 
182
- export async function buildToolStatusMessage(pi: ExtensionAPI) {
183
- const summary = getToolStatusSummary(pi);
200
+ export async function buildToolStatusMessage(pi: ExtensionAPI, owner: object) {
201
+ const summary = getToolStatusSummary(pi, owner);
184
202
  const persistedSetting = await persistedSettingLabel();
185
203
  return sanitizeChromeDevtoolsDisplay(
186
204
  [
187
205
  `Chrome DevTools tools available: ${formatRuntimeStatus(summary)}`,
188
206
  `Tool exposure: ${chromeDevtoolsToolExposureMode(pi)}`,
189
- `Loaded capability tools this session: ${summary.loadedChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
207
+ `Loaded capability tools this session: ${summary.loadedChromeToolCount}/${summary.capabilityCount}`,
208
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · confirmation required for every call" : "disabled · experimental"}`,
190
209
  `Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
191
210
  `Persisted tool catalog: ${persistedSetting}`,
192
- ...browserSettingsStatusLines(),
211
+ ...browserSettingsStatusLines(owner),
193
212
  ...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
194
213
  `Other active tools preserved: ${summary.activeNonChromeToolCount}`,
195
- `Endpoint: ${devToolsEndpoint()}`,
196
- `Endpoint source: ${endpointSourceLabel()}`,
197
- `Launch mode: ${launchModeLabel()}`,
198
- ...launchAttemptLines(),
214
+ `Endpoint: ${devToolsEndpoint(owner)}`,
215
+ `Endpoint source: ${endpointSourceLabel(owner)}`,
216
+ `Launch mode: ${launchModeLabel(owner)}`,
217
+ ...launchAttemptLines(owner),
199
218
  ].join("\n"),
200
219
  );
201
220
  }
202
221
 
203
- export function buildQuickstartMessage() {
204
- return buildSettingsSetupMessage();
222
+ export function buildQuickstartMessage(owner: object) {
223
+ return buildSettingsSetupMessage(owner);
205
224
  }
206
225
 
207
- export function buildBrowserStatusMessage() {
208
- const lifecycle = browserLifecycleState();
226
+ export function buildBrowserStatusMessage(owner?: object) {
227
+ const browser = browserSettingsForOwner(owner);
228
+ const lifecycle = browserLifecycleState(owner);
229
+ const webMcpIsEnabled = owner ? webMcpEnabled(owner) : false;
209
230
  const browserState =
210
231
  lifecycle === "starting"
211
232
  ? "starting managed browser"
@@ -221,36 +242,46 @@ export function buildBrowserStatusMessage() {
221
242
  [
222
243
  `Browser: ${browserState}`,
223
244
  "Viewing this status does not probe the endpoint or launch Chrome.",
224
- `Endpoint: ${devToolsEndpoint()}`,
225
- `Endpoint source: ${endpointSourceLabel()}`,
226
- `Launch mode: ${launchModeLabel()}`,
227
- `Unpacked extensions: ${state.extensionPaths.length} (${state.extensionPathsSource})`,
228
- ...(state.extensionPaths.length > 0
245
+ `Endpoint: ${devToolsEndpoint(owner)}`,
246
+ `Endpoint source: ${endpointSourceLabel(owner)}`,
247
+ `Launch mode: ${launchModeLabel(owner)}`,
248
+ `Unpacked extensions: ${browser.extensionPaths.length} (${browser.extensionPathsSource})`,
249
+ `WebMCP: ${webMcpIsEnabled ? "enabled · experimental" : "disabled · experimental"}`,
250
+ ...(webMcpIsEnabled && !managedBrowserForOwner(owner)?.ready
251
+ ? [
252
+ "WebMCP warning: attached browser profiles may contain everyday authenticated sessions and sensitive state.",
253
+ ]
254
+ : []),
255
+ ...(browser.extensionPaths.length > 0
229
256
  ? ["Unpacked extensions execute trusted browser code in an isolated managed browser."]
230
257
  : []),
231
- ...launchAttemptLines(),
232
- ...(needsRecovery ? [launchHint(), endpointConfigHint()] : []),
258
+ ...launchAttemptLines(owner),
259
+ ...(needsRecovery ? [launchHint(owner), endpointConfigHint()] : []),
233
260
  ].join("\n"),
234
261
  );
235
262
  }
236
263
 
237
- export function buildSettingsSetupMessage() {
264
+ export function buildSettingsSetupMessage(owner: object) {
238
265
  return sanitizeChromeDevtoolsDisplay(
239
266
  [
240
- `Chrome DevTools endpoint: ${devToolsEndpoint()}`,
241
- `Endpoint source: ${endpointSourceLabel()}`,
242
- `Launch mode: ${launchModeLabel()}`,
243
- ...browserSettingsStatusLines(),
244
- launchHint(),
245
- browserCandidateHint(),
246
- ...launchAttemptLines(),
267
+ `Chrome DevTools endpoint: ${devToolsEndpoint(owner)}`,
268
+ `Endpoint source: ${endpointSourceLabel(owner)}`,
269
+ `Launch mode: ${launchModeLabel(owner)}`,
270
+ ...browserSettingsStatusLines(owner),
271
+ launchHint(owner),
272
+ browserCandidateHint(owner),
273
+ ...launchAttemptLines(owner),
247
274
  endpointConfigHint(),
248
275
  ].join("\n"),
249
276
  );
250
277
  }
251
278
 
252
279
  export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_000) {
253
- const sanitized = Array.from(stripVTControlCharacters(value), (character) => {
280
+ const withoutBidi = stripVTControlCharacters(value).replace(
281
+ /[\u202a-\u202e\u2066-\u2069]/gu,
282
+ "�",
283
+ );
284
+ const sanitized = Array.from(withoutBidi, (character) => {
254
285
  const codePoint = character.codePointAt(0) ?? 0;
255
286
  const unsafeControl =
256
287
  (codePoint >= 0 && codePoint <= 8) ||
@@ -262,10 +293,12 @@ export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_
262
293
  return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}…`;
263
294
  }
264
295
 
265
- function browserSettingsStatusLines() {
296
+ function browserSettingsStatusLines(owner: object) {
297
+ const browser = browserSettingsForOwner(owner);
298
+ const extensionPaths = browser.extensionPaths;
266
299
  const extensionLines =
267
- state.extensionPaths.length > 0
268
- ? state.extensionPaths.map((extensionPath) => ` - ${extensionPath}`)
300
+ extensionPaths.length > 0
301
+ ? extensionPaths.map((extensionPath) => ` - ${extensionPath}`)
269
302
  : [" - none"];
270
303
  return [
271
304
  `Settings file: ${state.settingsFilePath ?? settingsFilePath()} (user)`,
@@ -274,12 +307,13 @@ function browserSettingsStatusLines() {
274
307
  `Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`,
275
308
  ]
276
309
  : []),
277
- `Auto-launch: ${state.autoLaunchEnabled ? "on" : "off"} (${state.autoLaunchSource})`,
278
- `Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
279
- `Unpacked extensions (${state.extensionPathsSource}):`,
310
+ `Auto-launch: ${browser.autoLaunchEnabled ? "on" : "off"} (${browser.autoLaunchSource})`,
311
+ `Browser executable: ${browser.executablePath ?? "automatic discovery"} (${browser.executablePathSource})`,
312
+ `Unpacked extensions (${browser.extensionPathsSource}):`,
280
313
  ...extensionLines,
314
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · every call requires confirmation" : "disabled · experimental"}`,
281
315
  "Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
282
- ...(state.extensionPaths.length > 0
316
+ ...(extensionPaths.length > 0
283
317
  ? [
284
318
  "Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code.",
285
319
  ]
@@ -287,9 +321,10 @@ function browserSettingsStatusLines() {
287
321
  ];
288
322
  }
289
323
 
290
- export function buildCommandGuide() {
324
+ export function buildCommandGuide(owner: object) {
291
325
  return [
292
326
  "Chrome DevTools commands:",
327
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled" : "disabled"} · experimental · every page-provided call requires confirmation`,
293
328
  "/chrome-devtools — open this menu",
294
329
  "/chrome-devtools help — show command usage",
295
330
  "/chrome-devtools quickstart — show endpoint and launch help",
@@ -302,8 +337,8 @@ export function buildCommandGuide() {
302
337
  ].join("\n");
303
338
  }
304
339
 
305
- export function allChromeDevtoolsTools() {
306
- return [...CHROME_DEVTOOLS_TOOL_NAMES];
340
+ export function allChromeDevtoolsTools(owner: object) {
341
+ return effectiveCatalog(owner);
307
342
  }
308
343
 
309
344
  export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevToolsToolName>) {
@@ -311,7 +346,7 @@ export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevT
311
346
  }
312
347
 
313
348
  function formatRuntimeStatus(summary: ToolStatusSummary) {
314
- return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} available)`;
349
+ return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${summary.capabilityCount} available)`;
315
350
  }
316
351
 
317
352
  async function persistedSettingLabel() {
@@ -334,6 +369,12 @@ function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
334
369
  return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
335
370
  }
336
371
 
372
+ function effectiveCatalog(owner: object): ChromeDevToolsToolName[] {
373
+ return webMcpEnabled(owner)
374
+ ? [...CHROME_DEVTOOLS_TOOL_NAMES]
375
+ : [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
376
+ }
377
+
337
378
  async function persistSettings(selectedTools: readonly ChromeDevToolsToolName[]) {
338
379
  await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
339
380
  }
package/src/tools.ts CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  listPages,
7
7
  resolvePage,
8
8
  resolvePageForNavigation,
9
+ setActivePageId,
9
10
  textResult,
10
11
  withCdp,
11
12
  } from "./cdp-client.js";
12
13
  import { renderScreenshotResult, renderTextResult, renderToolCall, withStatus } from "./render.js";
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,10 +38,10 @@ 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);
44
- state.activePageId = page.id;
43
+ const page = await getPage(params.pageId, { sessionOwner: ctx.sessionManager, signal });
44
+ setActivePageId(ctx.sessionManager, page.id);
45
45
  return textResult(`Selected page ${page.id}: ${page.title}\n${page.url}`, {
46
46
  page: formatPage(page),
47
47
  });
@@ -62,15 +62,18 @@ 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 });
71
74
  });
72
75
 
73
- state.activePageId = page.id;
76
+ setActivePageId(ctx.sessionManager, page.id);
74
77
  const action = created ? "Created page and navigated" : "Navigated";
75
78
  return textResult(`${action} ${page.id} to ${params.url}`, {
76
79
  created,
@@ -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,
@@ -107,12 +113,79 @@ export const evaluateTool = defineTool({
107
113
  }),
108
114
  );
109
115
 
110
- state.activePageId = page.id;
116
+ setActivePageId(ctx.sessionManager, page.id);
111
117
  return textResult(JSON.stringify(result, null, 2), { page: formatPage(page), result });
112
118
  });
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");
@@ -162,7 +238,7 @@ export const screenshotTool = defineTool({
162
238
  });
163
239
  });
164
240
 
165
- state.activePageId = page.id;
241
+ setActivePageId(ctx.sessionManager, page.id);
166
242
  const savedScreenshot = await saveScreenshot(result.data, params.savePath, ctx.cwd, signal);
167
243
  return {
168
244
  content: [