@narumitw/pi-chrome-devtools 0.52.2 → 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.
- package/README.md +78 -17
- package/dist/chunks/{browser-settings-menu-VI2I3TXI.js → browser-settings-menu-N5O65I7S.js} +81 -7
- package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
- package/dist/chunks/chunk-B7B63PWB.js +465 -0
- package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
- package/dist/chunks/{chunk-CC37WUUE.js → chunk-GVBXT3FB.js} +369 -461
- package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
- package/dist/chunks/chunk-RTECHAEZ.js +373 -0
- package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
- package/dist/chunks/{menu-LLR455UX.js → menu-UBHW6UP2.js} +75 -41
- package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
- package/dist/chunks/tools-V7OR5KJ3.js +945 -0
- package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
- package/dist/index.ts +166 -196
- package/dist/index.ts.map +4 -4
- package/package.json +4 -3
- package/src/browser-manager.ts +282 -109
- package/src/browser-settings-menu.ts +98 -4
- package/src/cdp-client.ts +329 -83
- package/src/chrome-devtools.ts +61 -13
- package/src/lazy-tools.ts +105 -8
- package/src/menu.ts +83 -41
- package/src/render.ts +12 -1
- package/src/runtime.ts +92 -0
- package/src/settings.ts +47 -10
- package/src/tool-names.ts +16 -1
- package/src/tool-selector.ts +79 -36
- package/src/tools.ts +86 -10
- package/src/webmcp/discovery.ts +235 -0
- package/src/webmcp/policy.ts +402 -0
- package/src/webmcp/protocol.ts +414 -0
- package/src/webmcp/tools.ts +190 -0
- package/dist/chunks/browser-settings-menu-VI2I3TXI.js.map +0 -7
- package/dist/chunks/chunk-CC37WUUE.js.map +0 -7
- package/dist/chunks/menu-LLR455UX.js.map +0 -7
package/src/runtime.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface ManagedBrowser {
|
|
|
57
57
|
exited: boolean;
|
|
58
58
|
ready: boolean;
|
|
59
59
|
ownerGeneration: number;
|
|
60
|
+
sessionOwner?: object;
|
|
60
61
|
cleanupPromise?: Promise<void>;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -98,6 +99,97 @@ export const state: ChromeDevToolsState = {
|
|
|
98
99
|
sessionController: new AbortController(),
|
|
99
100
|
};
|
|
100
101
|
|
|
102
|
+
interface WebMcpSessionRuntime {
|
|
103
|
+
controller: AbortController;
|
|
104
|
+
enabled: boolean;
|
|
105
|
+
controllers: Set<AbortController>;
|
|
106
|
+
generation: number;
|
|
107
|
+
sessionGeneration: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const webMcpRuntimeByOwner = new WeakMap<object, WebMcpSessionRuntime>();
|
|
111
|
+
|
|
112
|
+
export function setWebMcpSessionOwner(owner: object) {
|
|
113
|
+
const runtime = webMcpRuntime(owner);
|
|
114
|
+
runtime.enabled = false;
|
|
115
|
+
runtime.sessionGeneration += 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function beginWebMcpOperation(owner: object, toolSignal?: AbortSignal) {
|
|
119
|
+
const runtime = webMcpRuntime(owner);
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
runtime.controllers.add(controller);
|
|
122
|
+
const signals = [controller.signal, runtime.controller.signal];
|
|
123
|
+
if (toolSignal) signals.push(toolSignal);
|
|
124
|
+
const signal = AbortSignal.any(signals);
|
|
125
|
+
return {
|
|
126
|
+
signal,
|
|
127
|
+
owner,
|
|
128
|
+
sessionGeneration: runtime.sessionGeneration,
|
|
129
|
+
webMcpGeneration: runtime.generation,
|
|
130
|
+
dispose() {
|
|
131
|
+
runtime.controllers.delete(controller);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function invalidateWebMcpOperations(owner: object, reason: string) {
|
|
137
|
+
const runtime = webMcpRuntime(owner);
|
|
138
|
+
runtime.generation += 1;
|
|
139
|
+
const controllers = [...runtime.controllers];
|
|
140
|
+
runtime.controllers.clear();
|
|
141
|
+
const error = new DOMException(reason, "AbortError");
|
|
142
|
+
runtime.controller.abort(error);
|
|
143
|
+
runtime.controller = new AbortController();
|
|
144
|
+
for (const controller of controllers) controller.abort(error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function currentWebMcpGeneration(owner: object) {
|
|
148
|
+
return webMcpRuntime(owner).generation;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function webMcpSessionSignal(owner: object) {
|
|
152
|
+
return webMcpRuntime(owner).controller.signal;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function applyRuntimeWebMcpSetting(enabled: boolean, owner: object) {
|
|
156
|
+
const runtime = webMcpRuntime(owner);
|
|
157
|
+
if (runtime.enabled !== enabled) {
|
|
158
|
+
invalidateWebMcpOperations(owner, `WebMCP ${enabled ? "enabled" : "disabled"}`);
|
|
159
|
+
}
|
|
160
|
+
runtime.enabled = enabled;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function webMcpEnabled(owner: object) {
|
|
164
|
+
return webMcpRuntime(owner).enabled;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function webMcpOperationIsCurrent(operation: {
|
|
168
|
+
owner: object;
|
|
169
|
+
sessionGeneration: number;
|
|
170
|
+
webMcpGeneration: number;
|
|
171
|
+
}) {
|
|
172
|
+
const runtime = webMcpRuntime(operation.owner);
|
|
173
|
+
return (
|
|
174
|
+
operation.sessionGeneration === runtime.sessionGeneration &&
|
|
175
|
+
operation.webMcpGeneration === runtime.generation
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function webMcpRuntime(owner: object) {
|
|
180
|
+
const existing = webMcpRuntimeByOwner.get(owner);
|
|
181
|
+
if (existing) return existing;
|
|
182
|
+
const created: WebMcpSessionRuntime = {
|
|
183
|
+
controller: new AbortController(),
|
|
184
|
+
controllers: new Set(),
|
|
185
|
+
enabled: false,
|
|
186
|
+
generation: 0,
|
|
187
|
+
sessionGeneration: 0,
|
|
188
|
+
};
|
|
189
|
+
webMcpRuntimeByOwner.set(owner, created);
|
|
190
|
+
return created;
|
|
191
|
+
}
|
|
192
|
+
|
|
101
193
|
export function applyRuntimeBrowserSettings(
|
|
102
194
|
browser: EffectiveBrowserSettings,
|
|
103
195
|
paths: { user: string; project?: string },
|
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 =
|
|
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
|
|
316
|
+
function projectOwnedSettingsWarnings(
|
|
311
317
|
document: Record<string, unknown>,
|
|
312
318
|
scope: "project" | "user",
|
|
313
319
|
filePath: string,
|
|
314
320
|
) {
|
|
315
|
-
if (scope !== "project"
|
|
316
|
-
const browser = document.browser;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
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
|
+
}
|
package/src/tool-selector.ts
CHANGED
|
@@ -9,15 +9,22 @@ import {
|
|
|
9
9
|
launchAttemptLines,
|
|
10
10
|
launchHint,
|
|
11
11
|
launchModeLabel,
|
|
12
|
+
managedBrowserForOwner,
|
|
12
13
|
} from "./browser-manager.js";
|
|
13
14
|
import {
|
|
14
15
|
applyAvailableChromeDevtoolsTools,
|
|
15
16
|
availableChromeDevtoolsTools,
|
|
16
17
|
CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
18
|
+
chromeDevtoolsToolExposureMode,
|
|
17
19
|
} from "./lazy-tools.js";
|
|
18
|
-
import { state } from "./runtime.js";
|
|
20
|
+
import { invalidateWebMcpOperations, state, webMcpEnabled } from "./runtime.js";
|
|
19
21
|
import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
|
|
20
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
CHROME_DEVTOOLS_TOOL_NAMES,
|
|
24
|
+
type ChromeDevToolsToolName,
|
|
25
|
+
CORE_CHROME_DEVTOOLS_TOOL_NAMES,
|
|
26
|
+
isWebMcpToolName,
|
|
27
|
+
} from "./tool-names.js";
|
|
21
28
|
|
|
22
29
|
type CommandContext = ExtensionCommandContext;
|
|
23
30
|
|
|
@@ -34,6 +41,7 @@ interface ToolStatusSummary {
|
|
|
34
41
|
availableChromeToolCount: number;
|
|
35
42
|
loadedChromeToolCount: number;
|
|
36
43
|
activeNonChromeToolCount: number;
|
|
44
|
+
capabilityCount: number;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
type ToolSelectionSaveResult = "saved" | "active-tools-changed" | "failed";
|
|
@@ -47,9 +55,9 @@ export async function updateChromeDevtoolsTools(
|
|
|
47
55
|
const generation = state.sessionGeneration;
|
|
48
56
|
const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
|
|
49
57
|
if (result !== "saved" || generation !== state.sessionGeneration) return;
|
|
50
|
-
const status = await buildToolStatusMessage(pi);
|
|
58
|
+
const status = await buildToolStatusMessage(pi, ctx.sessionManager);
|
|
51
59
|
if (generation !== state.sessionGeneration) return;
|
|
52
|
-
ctx.ui.notify(`Chrome DevTools
|
|
60
|
+
ctx.ui.notify(`Chrome DevTools tool catalog ${action}.\n\n${status}`, "info");
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export async function setSelectedChromeDevtoolsTools(
|
|
@@ -108,6 +116,14 @@ async function transactSelectedToolsNow(
|
|
|
108
116
|
const previousActiveTools = pi.getActiveTools();
|
|
109
117
|
const previousAvailableTools = availableChromeDevtoolsTools(pi);
|
|
110
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
|
+
}
|
|
111
127
|
applyChromeDevtoolsTools(pi, selectedTools);
|
|
112
128
|
await persistSettings(selectedTools);
|
|
113
129
|
return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
|
|
@@ -121,7 +137,11 @@ async function transactSelectedToolsNow(
|
|
|
121
137
|
const previousLoadedChromeTools = previousActiveTools.filter((name) =>
|
|
122
138
|
CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName),
|
|
123
139
|
);
|
|
124
|
-
|
|
140
|
+
const restoredChromeTools =
|
|
141
|
+
chromeDevtoolsToolExposureMode(pi) === "eager"
|
|
142
|
+
? previousAvailableTools
|
|
143
|
+
: previousLoadedChromeTools;
|
|
144
|
+
pi.setActiveTools(unique([...currentNonChromeTools, ...restoredChromeTools]));
|
|
125
145
|
} catch (caught) {
|
|
126
146
|
rollbackError = caught;
|
|
127
147
|
}
|
|
@@ -149,7 +169,7 @@ export function applyChromeDevtoolsTools(
|
|
|
149
169
|
applyAvailableChromeDevtoolsTools(pi, selectedTools);
|
|
150
170
|
}
|
|
151
171
|
|
|
152
|
-
function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
172
|
+
function getToolStatusSummary(pi: ExtensionAPI, owner: object): ToolStatusSummary {
|
|
153
173
|
const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
154
174
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
155
175
|
const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
|
|
@@ -159,8 +179,9 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
|
159
179
|
const activeNonChromeToolCount = Array.from(activeToolNames).filter(
|
|
160
180
|
(name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
161
181
|
).length;
|
|
182
|
+
const capabilityCount = effectiveCatalog(owner).length;
|
|
162
183
|
const availabilityStatus =
|
|
163
|
-
availableChromeToolCount ===
|
|
184
|
+
availableChromeToolCount === capabilityCount
|
|
164
185
|
? "enabled"
|
|
165
186
|
: availableChromeToolCount === 0
|
|
166
187
|
? "disabled"
|
|
@@ -171,35 +192,39 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
|
171
192
|
availableChromeToolCount,
|
|
172
193
|
loadedChromeToolCount,
|
|
173
194
|
activeNonChromeToolCount,
|
|
195
|
+
capabilityCount,
|
|
174
196
|
};
|
|
175
197
|
}
|
|
176
198
|
|
|
177
|
-
export async function buildToolStatusMessage(pi: ExtensionAPI) {
|
|
178
|
-
const summary = getToolStatusSummary(pi);
|
|
199
|
+
export async function buildToolStatusMessage(pi: ExtensionAPI, owner: object) {
|
|
200
|
+
const summary = getToolStatusSummary(pi, owner);
|
|
179
201
|
const persistedSetting = await persistedSettingLabel();
|
|
180
202
|
return sanitizeChromeDevtoolsDisplay(
|
|
181
203
|
[
|
|
182
|
-
`Chrome DevTools tools available
|
|
183
|
-
`
|
|
204
|
+
`Chrome DevTools tools available: ${formatRuntimeStatus(summary)}`,
|
|
205
|
+
`Tool exposure: ${chromeDevtoolsToolExposureMode(pi)}`,
|
|
206
|
+
`Loaded capability tools this session: ${summary.loadedChromeToolCount}/${summary.capabilityCount}`,
|
|
207
|
+
`WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · confirmation required for every call" : "disabled · experimental"}`,
|
|
184
208
|
`Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
|
|
185
|
-
`Persisted
|
|
186
|
-
...browserSettingsStatusLines(),
|
|
209
|
+
`Persisted tool catalog: ${persistedSetting}`,
|
|
210
|
+
...browserSettingsStatusLines(owner),
|
|
187
211
|
...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
|
|
188
212
|
`Other active tools preserved: ${summary.activeNonChromeToolCount}`,
|
|
189
|
-
`Endpoint: ${devToolsEndpoint()}`,
|
|
213
|
+
`Endpoint: ${devToolsEndpoint(owner)}`,
|
|
190
214
|
`Endpoint source: ${endpointSourceLabel()}`,
|
|
191
|
-
`Launch mode: ${launchModeLabel()}`,
|
|
192
|
-
...launchAttemptLines(),
|
|
215
|
+
`Launch mode: ${launchModeLabel(owner)}`,
|
|
216
|
+
...launchAttemptLines(owner),
|
|
193
217
|
].join("\n"),
|
|
194
218
|
);
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
export function buildQuickstartMessage() {
|
|
198
|
-
return buildSettingsSetupMessage();
|
|
221
|
+
export function buildQuickstartMessage(owner: object) {
|
|
222
|
+
return buildSettingsSetupMessage(owner);
|
|
199
223
|
}
|
|
200
224
|
|
|
201
|
-
export function buildBrowserStatusMessage() {
|
|
202
|
-
const lifecycle = browserLifecycleState();
|
|
225
|
+
export function buildBrowserStatusMessage(owner?: object) {
|
|
226
|
+
const lifecycle = browserLifecycleState(owner);
|
|
227
|
+
const webMcpIsEnabled = owner ? webMcpEnabled(owner) : false;
|
|
203
228
|
const browserState =
|
|
204
229
|
lifecycle === "starting"
|
|
205
230
|
? "starting managed browser"
|
|
@@ -215,36 +240,46 @@ export function buildBrowserStatusMessage() {
|
|
|
215
240
|
[
|
|
216
241
|
`Browser: ${browserState}`,
|
|
217
242
|
"Viewing this status does not probe the endpoint or launch Chrome.",
|
|
218
|
-
`Endpoint: ${devToolsEndpoint()}`,
|
|
243
|
+
`Endpoint: ${devToolsEndpoint(owner)}`,
|
|
219
244
|
`Endpoint source: ${endpointSourceLabel()}`,
|
|
220
|
-
`Launch mode: ${launchModeLabel()}`,
|
|
245
|
+
`Launch mode: ${launchModeLabel(owner)}`,
|
|
221
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
|
+
: []),
|
|
222
253
|
...(state.extensionPaths.length > 0
|
|
223
254
|
? ["Unpacked extensions execute trusted browser code in an isolated managed browser."]
|
|
224
255
|
: []),
|
|
225
|
-
...launchAttemptLines(),
|
|
256
|
+
...launchAttemptLines(owner),
|
|
226
257
|
...(needsRecovery ? [launchHint(), endpointConfigHint()] : []),
|
|
227
258
|
].join("\n"),
|
|
228
259
|
);
|
|
229
260
|
}
|
|
230
261
|
|
|
231
|
-
export function buildSettingsSetupMessage() {
|
|
262
|
+
export function buildSettingsSetupMessage(owner: object) {
|
|
232
263
|
return sanitizeChromeDevtoolsDisplay(
|
|
233
264
|
[
|
|
234
|
-
`Chrome DevTools endpoint: ${devToolsEndpoint()}`,
|
|
265
|
+
`Chrome DevTools endpoint: ${devToolsEndpoint(owner)}`,
|
|
235
266
|
`Endpoint source: ${endpointSourceLabel()}`,
|
|
236
|
-
`Launch mode: ${launchModeLabel()}`,
|
|
237
|
-
...browserSettingsStatusLines(),
|
|
267
|
+
`Launch mode: ${launchModeLabel(owner)}`,
|
|
268
|
+
...browserSettingsStatusLines(owner),
|
|
238
269
|
launchHint(),
|
|
239
270
|
browserCandidateHint(),
|
|
240
|
-
...launchAttemptLines(),
|
|
271
|
+
...launchAttemptLines(owner),
|
|
241
272
|
endpointConfigHint(),
|
|
242
273
|
].join("\n"),
|
|
243
274
|
);
|
|
244
275
|
}
|
|
245
276
|
|
|
246
277
|
export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_000) {
|
|
247
|
-
const
|
|
278
|
+
const withoutBidi = stripVTControlCharacters(value).replace(
|
|
279
|
+
/[\u202a-\u202e\u2066-\u2069]/gu,
|
|
280
|
+
"�",
|
|
281
|
+
);
|
|
282
|
+
const sanitized = Array.from(withoutBidi, (character) => {
|
|
248
283
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
249
284
|
const unsafeControl =
|
|
250
285
|
(codePoint >= 0 && codePoint <= 8) ||
|
|
@@ -256,7 +291,7 @@ export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_
|
|
|
256
291
|
return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}…`;
|
|
257
292
|
}
|
|
258
293
|
|
|
259
|
-
function browserSettingsStatusLines() {
|
|
294
|
+
function browserSettingsStatusLines(owner: object) {
|
|
260
295
|
const extensionLines =
|
|
261
296
|
state.extensionPaths.length > 0
|
|
262
297
|
? state.extensionPaths.map((extensionPath) => ` - ${extensionPath}`)
|
|
@@ -272,6 +307,7 @@ function browserSettingsStatusLines() {
|
|
|
272
307
|
`Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
|
|
273
308
|
`Unpacked extensions (${state.extensionPathsSource}):`,
|
|
274
309
|
...extensionLines,
|
|
310
|
+
`WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · every call requires confirmation" : "disabled · experimental"}`,
|
|
275
311
|
"Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
|
|
276
312
|
...(state.extensionPaths.length > 0
|
|
277
313
|
? [
|
|
@@ -281,23 +317,24 @@ function browserSettingsStatusLines() {
|
|
|
281
317
|
];
|
|
282
318
|
}
|
|
283
319
|
|
|
284
|
-
export function buildCommandGuide() {
|
|
320
|
+
export function buildCommandGuide(owner: object) {
|
|
285
321
|
return [
|
|
286
322
|
"Chrome DevTools commands:",
|
|
323
|
+
`WebMCP: ${webMcpEnabled(owner) ? "enabled" : "disabled"} · experimental · every page-provided call requires confirmation`,
|
|
287
324
|
"/chrome-devtools — open this menu",
|
|
288
325
|
"/chrome-devtools help — show command usage",
|
|
289
326
|
"/chrome-devtools quickstart — show endpoint and launch help",
|
|
290
327
|
"/chrome-devtools status — show tool and settings status",
|
|
291
328
|
"/chrome-devtools settings — edit browser connection settings",
|
|
292
|
-
"/chrome-devtools tools — choose
|
|
329
|
+
"/chrome-devtools tools — choose available Chrome DevTools tools",
|
|
293
330
|
"/chrome-devtools toggle|select — compatibility aliases for tools",
|
|
294
|
-
"/chrome-devtools enable|on — make all Chrome DevTools tools available
|
|
331
|
+
"/chrome-devtools enable|on — make all Chrome DevTools tools available",
|
|
295
332
|
"/chrome-devtools disable|off — make all Chrome DevTools capability tools unavailable",
|
|
296
333
|
].join("\n");
|
|
297
334
|
}
|
|
298
335
|
|
|
299
|
-
export function allChromeDevtoolsTools() {
|
|
300
|
-
return
|
|
336
|
+
export function allChromeDevtoolsTools(owner: object) {
|
|
337
|
+
return effectiveCatalog(owner);
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevToolsToolName>) {
|
|
@@ -305,7 +342,7 @@ export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevT
|
|
|
305
342
|
}
|
|
306
343
|
|
|
307
344
|
function formatRuntimeStatus(summary: ToolStatusSummary) {
|
|
308
|
-
return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${
|
|
345
|
+
return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${summary.capabilityCount} available)`;
|
|
309
346
|
}
|
|
310
347
|
|
|
311
348
|
async function persistedSettingLabel() {
|
|
@@ -328,6 +365,12 @@ function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
|
|
|
328
365
|
return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
329
366
|
}
|
|
330
367
|
|
|
368
|
+
function effectiveCatalog(owner: object): ChromeDevToolsToolName[] {
|
|
369
|
+
return webMcpEnabled(owner)
|
|
370
|
+
? [...CHROME_DEVTOOLS_TOOL_NAMES]
|
|
371
|
+
: [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
|
|
372
|
+
}
|
|
373
|
+
|
|
331
374
|
async function persistSettings(selectedTools: readonly ChromeDevToolsToolName[]) {
|
|
332
375
|
await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
|
|
333
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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");
|