@narumitw/pi-chrome-devtools 0.53.2 → 0.53.4

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 (40) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{browser-settings-menu-IMB6HXSU.js → browser-settings-menu-FVMCCUPM.ts} +16 -42
  3. package/dist/chunks/browser-settings-menu-FVMCCUPM.ts.map +7 -0
  4. package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4B3EQHOH.ts} +2 -2
  5. package/dist/chunks/chunk-4B3EQHOH.ts.map +7 -0
  6. package/dist/chunks/{chunk-SRT3UUIP.js → chunk-LSJSDY2M.ts} +23 -74
  7. package/dist/chunks/chunk-LSJSDY2M.ts.map +7 -0
  8. package/dist/chunks/{chunk-LLXOCRNF.js → chunk-V7N3WI5B.ts} +12 -33
  9. package/dist/chunks/chunk-V7N3WI5B.ts.map +7 -0
  10. package/dist/chunks/{menu-UJETYMSB.js → menu-MTPSLDHZ.ts} +10 -23
  11. package/dist/chunks/menu-MTPSLDHZ.ts.map +7 -0
  12. package/dist/chunks/{tools-6P7EUX37.js → tools-OVNH45IJ.ts} +17 -59
  13. package/dist/chunks/tools-OVNH45IJ.ts.map +7 -0
  14. package/dist/index.ts +18 -42
  15. package/dist/index.ts.map +2 -2
  16. package/package.json +54 -55
  17. package/src/browser-manager.ts +872 -942
  18. package/src/browser-settings-menu.ts +315 -350
  19. package/src/cdp-client.ts +381 -398
  20. package/src/chrome-devtools.ts +205 -232
  21. package/src/display.ts +26 -29
  22. package/src/lazy-tools.ts +158 -181
  23. package/src/menu.ts +409 -446
  24. package/src/render.ts +72 -81
  25. package/src/runtime.ts +137 -140
  26. package/src/screenshot.ts +170 -186
  27. package/src/settings.ts +472 -523
  28. package/src/tool-names.ts +8 -14
  29. package/src/tool-selector.ts +242 -271
  30. package/src/tools.ts +218 -226
  31. package/src/webmcp/discovery.ts +184 -188
  32. package/src/webmcp/policy.ts +296 -329
  33. package/src/webmcp/protocol.ts +324 -337
  34. package/src/webmcp/tools.ts +136 -147
  35. package/dist/chunks/browser-settings-menu-IMB6HXSU.js.map +0 -7
  36. package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
  37. package/dist/chunks/chunk-LLXOCRNF.js.map +0 -7
  38. package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
  39. package/dist/chunks/menu-UJETYMSB.js.map +0 -7
  40. package/dist/chunks/tools-6P7EUX37.js.map +0 -7
@@ -3,44 +3,44 @@ import { CdpClient } from "../cdp-client.js";
3
3
  import { DEFAULT_TIMEOUT_MS, type DevToolsPage } from "../runtime.js";
4
4
 
5
5
  export interface WebMcpProtocolAnnotation {
6
- autosubmit?: boolean;
7
- consequential?: boolean;
8
- readOnly?: boolean;
9
- untrustedContent?: boolean;
6
+ autosubmit?: boolean;
7
+ consequential?: boolean;
8
+ readOnly?: boolean;
9
+ untrustedContent?: boolean;
10
10
  }
11
11
 
12
12
  export interface WebMcpProtocolTool {
13
- annotations?: WebMcpProtocolAnnotation;
14
- backendNodeId?: number;
15
- description: string;
16
- frameId: string;
17
- inputSchema?: Record<string, unknown>;
18
- name: string;
19
- title?: string;
13
+ annotations?: WebMcpProtocolAnnotation;
14
+ backendNodeId?: number;
15
+ description: string;
16
+ frameId: string;
17
+ inputSchema?: Record<string, unknown>;
18
+ name: string;
19
+ title?: string;
20
20
  }
21
21
 
22
22
  export interface WebMcpFrame {
23
- id: string;
24
- loaderId: string;
25
- securityOrigin?: string;
26
- url: string;
23
+ id: string;
24
+ loaderId: string;
25
+ securityOrigin?: string;
26
+ url: string;
27
27
  }
28
28
 
29
29
  export interface WebMcpInvocationResponse {
30
- errorText?: string;
31
- exception?: unknown;
32
- invocationId: string;
33
- output?: unknown;
34
- status: "Canceled" | "Completed" | "Error";
30
+ errorText?: string;
31
+ exception?: unknown;
32
+ invocationId: string;
33
+ output?: unknown;
34
+ status: "Canceled" | "Completed" | "Error";
35
35
  }
36
36
 
37
37
  export interface WebMcpIdentityWatch {
38
- dispose(): Promise<void>;
39
- signal: AbortSignal;
38
+ dispose(): Promise<void>;
39
+ signal: AbortSignal;
40
40
  }
41
41
 
42
42
  interface ProtocolDescription {
43
- domains?: unknown;
43
+ domains?: unknown;
44
44
  }
45
45
 
46
46
  const REQUIRED_COMMANDS = ["enable", "disable", "invokeTool", "cancelInvocation"];
@@ -48,379 +48,366 @@ const REQUIRED_EVENTS = ["toolsAdded", "toolsRemoved", "toolInvoked", "toolRespo
48
48
  const IDENTITY_WATCH_TIMEOUT_MS = DEFAULT_TIMEOUT_MS * 4;
49
49
 
50
50
  export async function requireWebMcpDomain(signal: AbortSignal, owner: object) {
51
- signal.throwIfAborted();
52
- const protocol = await fetchDevToolsJson<ProtocolDescription>(
53
- "/json/protocol",
54
- { signal },
55
- owner,
56
- );
57
- signal.throwIfAborted();
58
- const domain = findDomain(protocol.domains, "WebMCP");
59
- if (
60
- !domain ||
61
- !hasNamedEntries(domain.commands, REQUIRED_COMMANDS) ||
62
- !hasNamedEntries(domain.events, REQUIRED_EVENTS)
63
- ) {
64
- throw new Error(
65
- "This browser does not expose the experimental Chrome DevTools WebMCP domain. Use a compatible Chrome build and enable the website's required Origin Trial or WebMCP testing feature.",
66
- );
67
- }
51
+ signal.throwIfAborted();
52
+ const protocol = await fetchDevToolsJson<ProtocolDescription>("/json/protocol", { signal }, owner);
53
+ signal.throwIfAborted();
54
+ const domain = findDomain(protocol.domains, "WebMCP");
55
+ if (
56
+ !domain ||
57
+ !hasNamedEntries(domain.commands, REQUIRED_COMMANDS) ||
58
+ !hasNamedEntries(domain.events, REQUIRED_EVENTS)
59
+ ) {
60
+ throw new Error(
61
+ "This browser does not expose the experimental Chrome DevTools WebMCP domain. Use a compatible Chrome build and enable the website's required Origin Trial or WebMCP testing feature.",
62
+ );
63
+ }
68
64
  }
69
65
 
70
66
  export async function connectWebMcpPage(page: DevToolsPage, signal: AbortSignal) {
71
- if (!page.webSocketDebuggerUrl) throw new Error(`Page has no webSocketDebuggerUrl: ${page.id}`);
72
- return CdpClient.connect(page.webSocketDebuggerUrl, {
73
- signal: withDeadline(signal),
74
- });
67
+ if (!page.webSocketDebuggerUrl) throw new Error(`Page has no webSocketDebuggerUrl: ${page.id}`);
68
+ return CdpClient.connect(page.webSocketDebuggerUrl, {
69
+ signal: withDeadline(signal),
70
+ });
75
71
  }
76
72
 
77
73
  export async function cancelWebMcpInvocation(page: DevToolsPage, invocationId: string) {
78
- const cleanupSignal = AbortSignal.timeout(1_000);
79
- const client = await connectWebMcpPage(page, cleanupSignal);
80
- try {
81
- await client.send("WebMCP.cancelInvocation", { invocationId }, { signal: cleanupSignal });
82
- } finally {
83
- client.close();
84
- }
74
+ const cleanupSignal = AbortSignal.timeout(1_000);
75
+ const client = await connectWebMcpPage(page, cleanupSignal);
76
+ try {
77
+ await client.send("WebMCP.cancelInvocation", { invocationId }, { signal: cleanupSignal });
78
+ } finally {
79
+ client.close();
80
+ }
85
81
  }
86
82
 
87
83
  export async function enableWebMcpIdentityTracking(client: CdpClient, signal: AbortSignal) {
88
- await client.send("Page.enable", {}, { signal, timeoutMs: DEFAULT_TIMEOUT_MS });
84
+ await client.send("Page.enable", {}, { signal, timeoutMs: DEFAULT_TIMEOUT_MS });
89
85
  }
90
86
 
91
87
  export async function enableWebMcp(client: CdpClient, signal: AbortSignal) {
92
- const eventController = new AbortController();
93
- const eventSignal = AbortSignal.any([signal, eventController.signal]);
94
- const toolsAdded = client
95
- .waitForEvent(
96
- "WebMCP.toolsAdded",
97
- (value): value is { tools: WebMcpProtocolTool[] } => {
98
- const parsed = parseToolsAdded(value);
99
- if (!parsed) throw new Error("Chrome sent a malformed WebMCP.toolsAdded event");
100
- return true;
101
- },
102
- { signal: eventSignal, timeoutMs: DEFAULT_TIMEOUT_MS },
103
- )
104
- .then(
105
- (value) => ({ ok: true as const, value }),
106
- (error: unknown) => ({ error, ok: false as const }),
107
- );
108
- try {
109
- await client.send("WebMCP.enable", {}, { signal, timeoutMs: DEFAULT_TIMEOUT_MS });
110
- } catch (error) {
111
- eventController.abort();
112
- await toolsAdded;
113
- throw error;
114
- }
115
- try {
116
- const result = await toolsAdded;
117
- if (!result.ok) throw result.error;
118
- const parsed = parseToolsAdded(result.value);
119
- if (!parsed) throw new Error("Chrome sent a malformed WebMCP.toolsAdded event");
120
- return parsed.tools;
121
- } finally {
122
- eventController.abort();
123
- }
88
+ const eventController = new AbortController();
89
+ const eventSignal = AbortSignal.any([signal, eventController.signal]);
90
+ const toolsAdded = client
91
+ .waitForEvent(
92
+ "WebMCP.toolsAdded",
93
+ (value): value is { tools: WebMcpProtocolTool[] } => {
94
+ const parsed = parseToolsAdded(value);
95
+ if (!parsed) throw new Error("Chrome sent a malformed WebMCP.toolsAdded event");
96
+ return true;
97
+ },
98
+ { signal: eventSignal, timeoutMs: DEFAULT_TIMEOUT_MS },
99
+ )
100
+ .then(
101
+ (value) => ({ ok: true as const, value }),
102
+ (error: unknown) => ({ error, ok: false as const }),
103
+ );
104
+ try {
105
+ await client.send("WebMCP.enable", {}, { signal, timeoutMs: DEFAULT_TIMEOUT_MS });
106
+ } catch (error) {
107
+ eventController.abort();
108
+ await toolsAdded;
109
+ throw error;
110
+ }
111
+ try {
112
+ const result = await toolsAdded;
113
+ if (!result.ok) throw result.error;
114
+ const parsed = parseToolsAdded(result.value);
115
+ if (!parsed) throw new Error("Chrome sent a malformed WebMCP.toolsAdded event");
116
+ return parsed.tools;
117
+ } finally {
118
+ eventController.abort();
119
+ }
124
120
  }
125
121
 
126
122
  export async function disableWebMcp(client: CdpClient) {
127
- try {
128
- await client.send("WebMCP.disable", {}, { timeoutMs: 1_000 });
129
- } catch {
130
- // Best-effort cleanup: the target may already be detached or the domain may be unavailable.
131
- }
123
+ try {
124
+ await client.send("WebMCP.disable", {}, { timeoutMs: 1_000 });
125
+ } catch {
126
+ // Best-effort cleanup: the target may already be detached or the domain may be unavailable.
127
+ }
132
128
  }
133
129
 
134
130
  export async function readWebMcpFrames(client: CdpClient, signal: AbortSignal) {
135
- const result = await client.send<unknown>(
136
- "Page.getFrameTree",
137
- {},
138
- {
139
- signal,
140
- timeoutMs: DEFAULT_TIMEOUT_MS,
141
- },
142
- );
143
- const frames = parseFrameTree(result);
144
- if (!frames) throw new Error("Chrome sent a malformed Page.getFrameTree response");
145
- return frames;
131
+ const result = await client.send<unknown>(
132
+ "Page.getFrameTree",
133
+ {},
134
+ {
135
+ signal,
136
+ timeoutMs: DEFAULT_TIMEOUT_MS,
137
+ },
138
+ );
139
+ const frames = parseFrameTree(result);
140
+ if (!frames) throw new Error("Chrome sent a malformed Page.getFrameTree response");
141
+ return frames;
146
142
  }
147
143
 
148
144
  export function watchWebMcpIdentity(
149
- client: CdpClient,
150
- expected: { documentId: string; frameId: string; toolName: string },
151
- signal: AbortSignal,
145
+ client: CdpClient,
146
+ expected: { documentId: string; frameId: string; toolName: string },
147
+ signal: AbortSignal,
152
148
  ): WebMcpIdentityWatch {
153
- const stopController = new AbortController();
154
- const staleController = new AbortController();
155
- const watchSignal = AbortSignal.any([signal, stopController.signal]);
156
- const options = { signal: watchSignal, timeoutMs: IDENTITY_WATCH_TIMEOUT_MS };
157
- const settleWatch = (create: () => Promise<unknown>) => {
158
- let watch: Promise<unknown>;
159
- try {
160
- watch = create();
161
- } catch (error) {
162
- watch = Promise.reject(error);
163
- }
164
- return watch.then(
165
- () => {
166
- if (staleController.signal.aborted) return;
167
- staleController.abort(
168
- new DOMException(
169
- "The selected WebMCP document or tool changed before Chrome completed the invocation boundary.",
170
- "AbortError",
171
- ),
172
- );
173
- },
174
- (error) => {
175
- if (stopController.signal.aborted || signal.aborted || staleController.signal.aborted) {
176
- return;
177
- }
178
- staleController.abort(error);
179
- },
180
- );
181
- };
182
- const settledWatches = [
183
- settleWatch(() =>
184
- client.waitForEvent(
185
- "Page.frameNavigated",
186
- (value): value is unknown =>
187
- frameNavigationChangesIdentity(value, expected.frameId, expected.documentId),
188
- options,
189
- ),
190
- ),
191
- settleWatch(() =>
192
- client.waitForEvent(
193
- "WebMCP.toolsAdded",
194
- (value): value is unknown => toolChangeMatchesIdentity(value, expected),
195
- options,
196
- ),
197
- ),
198
- settleWatch(() =>
199
- client.waitForEvent(
200
- "WebMCP.toolsRemoved",
201
- (value): value is unknown => toolChangeMatchesIdentity(value, expected),
202
- options,
203
- ),
204
- ),
205
- ];
206
- return {
207
- signal: AbortSignal.any([signal, staleController.signal]),
208
- async dispose() {
209
- stopController.abort();
210
- await Promise.allSettled(settledWatches);
211
- },
212
- };
149
+ const stopController = new AbortController();
150
+ const staleController = new AbortController();
151
+ const watchSignal = AbortSignal.any([signal, stopController.signal]);
152
+ const options = { signal: watchSignal, timeoutMs: IDENTITY_WATCH_TIMEOUT_MS };
153
+ const settleWatch = (create: () => Promise<unknown>) => {
154
+ let watch: Promise<unknown>;
155
+ try {
156
+ watch = create();
157
+ } catch (error) {
158
+ watch = Promise.reject(error);
159
+ }
160
+ return watch.then(
161
+ () => {
162
+ if (staleController.signal.aborted) return;
163
+ staleController.abort(
164
+ new DOMException(
165
+ "The selected WebMCP document or tool changed before Chrome completed the invocation boundary.",
166
+ "AbortError",
167
+ ),
168
+ );
169
+ },
170
+ (error) => {
171
+ if (stopController.signal.aborted || signal.aborted || staleController.signal.aborted) {
172
+ return;
173
+ }
174
+ staleController.abort(error);
175
+ },
176
+ );
177
+ };
178
+ const settledWatches = [
179
+ settleWatch(() =>
180
+ client.waitForEvent(
181
+ "Page.frameNavigated",
182
+ (value): value is unknown => frameNavigationChangesIdentity(value, expected.frameId, expected.documentId),
183
+ options,
184
+ ),
185
+ ),
186
+ settleWatch(() =>
187
+ client.waitForEvent(
188
+ "WebMCP.toolsAdded",
189
+ (value): value is unknown => toolChangeMatchesIdentity(value, expected),
190
+ options,
191
+ ),
192
+ ),
193
+ settleWatch(() =>
194
+ client.waitForEvent(
195
+ "WebMCP.toolsRemoved",
196
+ (value): value is unknown => toolChangeMatchesIdentity(value, expected),
197
+ options,
198
+ ),
199
+ ),
200
+ ];
201
+ return {
202
+ signal: AbortSignal.any([signal, staleController.signal]),
203
+ async dispose() {
204
+ stopController.abort();
205
+ await Promise.allSettled(settledWatches);
206
+ },
207
+ };
213
208
  }
214
209
 
215
210
  export async function invokeWebMcpTool(
216
- client: CdpClient,
217
- request: { frameId: string; input: Record<string, unknown>; toolName: string },
218
- signal: AbortSignal,
219
- fallbackCancel?: (invocationId: string) => Promise<unknown>,
211
+ client: CdpClient,
212
+ request: { frameId: string; input: Record<string, unknown>; toolName: string },
213
+ signal: AbortSignal,
214
+ fallbackCancel?: (invocationId: string) => Promise<unknown>,
220
215
  ): Promise<WebMcpInvocationResponse> {
221
- signal.throwIfAborted();
222
- let invocationId: string | undefined;
223
- let cancellation: Promise<unknown> | undefined;
224
- const cancel = () => {
225
- if (!invocationId || cancellation) return;
226
- const invocationToCancel = invocationId;
227
- const recoverCancellation = () =>
228
- fallbackCancel
229
- ? fallbackCancel(invocationToCancel).catch(() => undefined)
230
- : Promise.resolve();
231
- try {
232
- cancellation = client
233
- .send("WebMCP.cancelInvocation", { invocationId }, { timeoutMs: 1_000 })
234
- .catch(recoverCancellation);
235
- } catch {
236
- cancellation = recoverCancellation();
237
- }
238
- };
239
- const onAbort = () => cancel();
240
- signal.addEventListener("abort", onAbort, { once: true });
241
- try {
242
- let response: unknown;
243
- try {
244
- response = await client.send<unknown>(
245
- "WebMCP.invokeTool",
246
- { frameId: request.frameId, toolName: request.toolName, input: request.input },
247
- { timeoutMs: DEFAULT_TIMEOUT_MS },
248
- );
249
- } catch (error) {
250
- if (signal.aborted) throw signal.reason;
251
- throw error;
252
- }
253
- invocationId = parseInvocationId(response);
254
- if (!invocationId) throw new Error("Chrome sent a malformed WebMCP.invokeTool response");
255
- if (signal.aborted) cancel();
256
- let event: WebMcpInvocationResponse;
257
- try {
258
- event = await client.waitForEvent(
259
- "WebMCP.toolResponded",
260
- (value): value is WebMcpInvocationResponse => {
261
- const parsed = parseToolResponded(value);
262
- if (!parsed) throw new Error("Chrome sent a malformed WebMCP.toolResponded event");
263
- return parsed.invocationId === invocationId;
264
- },
265
- { timeoutMs: DEFAULT_TIMEOUT_MS },
266
- );
267
- } catch (error) {
268
- cancel();
269
- await cancellation;
270
- if (signal.aborted) throw signal.reason;
271
- throw error;
272
- }
273
- if (signal.aborted) throw signal.reason;
274
- return event;
275
- } finally {
276
- signal.removeEventListener("abort", onAbort);
277
- await cancellation;
278
- }
216
+ signal.throwIfAborted();
217
+ let invocationId: string | undefined;
218
+ let cancellation: Promise<unknown> | undefined;
219
+ const cancel = () => {
220
+ if (!invocationId || cancellation) return;
221
+ const invocationToCancel = invocationId;
222
+ const recoverCancellation = () =>
223
+ fallbackCancel ? fallbackCancel(invocationToCancel).catch(() => undefined) : Promise.resolve();
224
+ try {
225
+ cancellation = client
226
+ .send("WebMCP.cancelInvocation", { invocationId }, { timeoutMs: 1_000 })
227
+ .catch(recoverCancellation);
228
+ } catch {
229
+ cancellation = recoverCancellation();
230
+ }
231
+ };
232
+ const onAbort = () => cancel();
233
+ signal.addEventListener("abort", onAbort, { once: true });
234
+ try {
235
+ let response: unknown;
236
+ try {
237
+ response = await client.send<unknown>(
238
+ "WebMCP.invokeTool",
239
+ { frameId: request.frameId, toolName: request.toolName, input: request.input },
240
+ { timeoutMs: DEFAULT_TIMEOUT_MS },
241
+ );
242
+ } catch (error) {
243
+ if (signal.aborted) throw signal.reason;
244
+ throw error;
245
+ }
246
+ invocationId = parseInvocationId(response);
247
+ if (!invocationId) throw new Error("Chrome sent a malformed WebMCP.invokeTool response");
248
+ if (signal.aborted) cancel();
249
+ let event: WebMcpInvocationResponse;
250
+ try {
251
+ event = await client.waitForEvent(
252
+ "WebMCP.toolResponded",
253
+ (value): value is WebMcpInvocationResponse => {
254
+ const parsed = parseToolResponded(value);
255
+ if (!parsed) throw new Error("Chrome sent a malformed WebMCP.toolResponded event");
256
+ return parsed.invocationId === invocationId;
257
+ },
258
+ { timeoutMs: DEFAULT_TIMEOUT_MS },
259
+ );
260
+ } catch (error) {
261
+ cancel();
262
+ await cancellation;
263
+ if (signal.aborted) throw signal.reason;
264
+ throw error;
265
+ }
266
+ if (signal.aborted) throw signal.reason;
267
+ return event;
268
+ } finally {
269
+ signal.removeEventListener("abort", onAbort);
270
+ await cancellation;
271
+ }
279
272
  }
280
273
 
281
274
  function findDomain(value: unknown, name: string): Record<string, unknown> | undefined {
282
- if (!Array.isArray(value)) return undefined;
283
- return value.find(
284
- (candidate): candidate is Record<string, unknown> =>
285
- isRecord(candidate) && candidate.domain === name,
286
- );
275
+ if (!Array.isArray(value)) return undefined;
276
+ return value.find(
277
+ (candidate): candidate is Record<string, unknown> => isRecord(candidate) && candidate.domain === name,
278
+ );
287
279
  }
288
280
 
289
281
  function hasNamedEntries(value: unknown, required: readonly string[]) {
290
- if (!Array.isArray(value)) return false;
291
- const names = new Set(
292
- value.flatMap((candidate) =>
293
- isRecord(candidate) && typeof candidate.name === "string" ? [candidate.name] : [],
294
- ),
295
- );
296
- return required.every((name) => names.has(name));
282
+ if (!Array.isArray(value)) return false;
283
+ const names = new Set(
284
+ value.flatMap((candidate) => (isRecord(candidate) && typeof candidate.name === "string" ? [candidate.name] : [])),
285
+ );
286
+ return required.every((name) => names.has(name));
297
287
  }
298
288
 
299
289
  function parseToolsAdded(value: unknown): { tools: WebMcpProtocolTool[] } | undefined {
300
- if (!isRecord(value) || !Array.isArray(value.tools)) return undefined;
301
- const tools: WebMcpProtocolTool[] = [];
302
- for (const candidate of value.tools) {
303
- const tool = parseTool(candidate);
304
- if (!tool) return undefined;
305
- tools.push(tool);
306
- }
307
- return { tools };
290
+ if (!isRecord(value) || !Array.isArray(value.tools)) return undefined;
291
+ const tools: WebMcpProtocolTool[] = [];
292
+ for (const candidate of value.tools) {
293
+ const tool = parseTool(candidate);
294
+ if (!tool) return undefined;
295
+ tools.push(tool);
296
+ }
297
+ return { tools };
308
298
  }
309
299
 
310
300
  function parseTool(value: unknown): WebMcpProtocolTool | undefined {
311
- if (
312
- !isRecord(value) ||
313
- typeof value.name !== "string" ||
314
- typeof value.description !== "string" ||
315
- typeof value.frameId !== "string" ||
316
- (value.inputSchema !== undefined && !isRecord(value.inputSchema)) ||
317
- (value.title !== undefined && typeof value.title !== "string") ||
318
- (value.backendNodeId !== undefined && typeof value.backendNodeId !== "number")
319
- ) {
320
- return undefined;
321
- }
322
- const annotations = parseAnnotations(value.annotations);
323
- if (value.annotations !== undefined && !annotations) return undefined;
324
- return {
325
- name: value.name,
326
- description: value.description,
327
- frameId: value.frameId,
328
- ...(value.title === undefined ? {} : { title: value.title }),
329
- ...(value.inputSchema === undefined ? {} : { inputSchema: value.inputSchema }),
330
- ...(annotations ? { annotations } : {}),
331
- ...(value.backendNodeId === undefined ? {} : { backendNodeId: value.backendNodeId }),
332
- };
301
+ if (
302
+ !isRecord(value) ||
303
+ typeof value.name !== "string" ||
304
+ typeof value.description !== "string" ||
305
+ typeof value.frameId !== "string" ||
306
+ (value.inputSchema !== undefined && !isRecord(value.inputSchema)) ||
307
+ (value.title !== undefined && typeof value.title !== "string") ||
308
+ (value.backendNodeId !== undefined && typeof value.backendNodeId !== "number")
309
+ ) {
310
+ return undefined;
311
+ }
312
+ const annotations = parseAnnotations(value.annotations);
313
+ if (value.annotations !== undefined && !annotations) return undefined;
314
+ return {
315
+ name: value.name,
316
+ description: value.description,
317
+ frameId: value.frameId,
318
+ ...(value.title === undefined ? {} : { title: value.title }),
319
+ ...(value.inputSchema === undefined ? {} : { inputSchema: value.inputSchema }),
320
+ ...(annotations ? { annotations } : {}),
321
+ ...(value.backendNodeId === undefined ? {} : { backendNodeId: value.backendNodeId }),
322
+ };
333
323
  }
334
324
 
335
325
  function parseAnnotations(value: unknown): WebMcpProtocolAnnotation | undefined {
336
- if (value === undefined) return undefined;
337
- if (!isRecord(value)) return undefined;
338
- const annotations: WebMcpProtocolAnnotation = {};
339
- for (const key of ["autosubmit", "consequential", "readOnly", "untrustedContent"] as const) {
340
- if (value[key] === undefined) continue;
341
- if (typeof value[key] !== "boolean") return undefined;
342
- annotations[key] = value[key];
343
- }
344
- return annotations;
326
+ if (value === undefined) return undefined;
327
+ if (!isRecord(value)) return undefined;
328
+ const annotations: WebMcpProtocolAnnotation = {};
329
+ for (const key of ["autosubmit", "consequential", "readOnly", "untrustedContent"] as const) {
330
+ if (value[key] === undefined) continue;
331
+ if (typeof value[key] !== "boolean") return undefined;
332
+ annotations[key] = value[key];
333
+ }
334
+ return annotations;
345
335
  }
346
336
 
347
337
  function parseFrameTree(value: unknown): WebMcpFrame[] | undefined {
348
- if (!isRecord(value) || !isRecord(value.frameTree)) return undefined;
349
- const frames: WebMcpFrame[] = [];
350
- const visit = (tree: unknown): boolean => {
351
- if (!isRecord(tree) || !isRecord(tree.frame)) return false;
352
- const frame = tree.frame;
353
- if (
354
- typeof frame.id !== "string" ||
355
- typeof frame.loaderId !== "string" ||
356
- typeof frame.url !== "string" ||
357
- (frame.securityOrigin !== undefined && typeof frame.securityOrigin !== "string")
358
- ) {
359
- return false;
360
- }
361
- frames.push({
362
- id: frame.id,
363
- loaderId: frame.loaderId,
364
- url: frame.url,
365
- ...(frame.securityOrigin === undefined ? {} : { securityOrigin: frame.securityOrigin }),
366
- });
367
- if (tree.childFrames === undefined) return true;
368
- return Array.isArray(tree.childFrames) && tree.childFrames.every(visit);
369
- };
370
- return visit(value.frameTree) ? frames : undefined;
338
+ if (!isRecord(value) || !isRecord(value.frameTree)) return undefined;
339
+ const frames: WebMcpFrame[] = [];
340
+ const visit = (tree: unknown): boolean => {
341
+ if (!isRecord(tree) || !isRecord(tree.frame)) return false;
342
+ const frame = tree.frame;
343
+ if (
344
+ typeof frame.id !== "string" ||
345
+ typeof frame.loaderId !== "string" ||
346
+ typeof frame.url !== "string" ||
347
+ (frame.securityOrigin !== undefined && typeof frame.securityOrigin !== "string")
348
+ ) {
349
+ return false;
350
+ }
351
+ frames.push({
352
+ id: frame.id,
353
+ loaderId: frame.loaderId,
354
+ url: frame.url,
355
+ ...(frame.securityOrigin === undefined ? {} : { securityOrigin: frame.securityOrigin }),
356
+ });
357
+ if (tree.childFrames === undefined) return true;
358
+ return Array.isArray(tree.childFrames) && tree.childFrames.every(visit);
359
+ };
360
+ return visit(value.frameTree) ? frames : undefined;
371
361
  }
372
362
 
373
363
  function frameNavigationChangesIdentity(value: unknown, frameId: string, documentId: string) {
374
- if (!isRecord(value) || !isRecord(value.frame)) {
375
- throw new Error("Chrome sent a malformed Page.frameNavigated event");
376
- }
377
- if (typeof value.frame.id !== "string" || typeof value.frame.loaderId !== "string") {
378
- throw new Error("Chrome sent a malformed Page.frameNavigated event");
379
- }
380
- return value.frame.id === frameId && value.frame.loaderId !== documentId;
364
+ if (!isRecord(value) || !isRecord(value.frame)) {
365
+ throw new Error("Chrome sent a malformed Page.frameNavigated event");
366
+ }
367
+ if (typeof value.frame.id !== "string" || typeof value.frame.loaderId !== "string") {
368
+ throw new Error("Chrome sent a malformed Page.frameNavigated event");
369
+ }
370
+ return value.frame.id === frameId && value.frame.loaderId !== documentId;
381
371
  }
382
372
 
383
- function toolChangeMatchesIdentity(
384
- value: unknown,
385
- expected: { frameId: string; toolName: string },
386
- ) {
387
- if (!isRecord(value) || !Array.isArray(value.tools)) {
388
- throw new Error("Chrome sent a malformed WebMCP tool-change event");
389
- }
390
- return value.tools.some((tool) => {
391
- if (!isRecord(tool) || typeof tool.frameId !== "string" || typeof tool.name !== "string") {
392
- throw new Error("Chrome sent a malformed WebMCP tool-change event");
393
- }
394
- return tool.frameId === expected.frameId && tool.name === expected.toolName;
395
- });
373
+ function toolChangeMatchesIdentity(value: unknown, expected: { frameId: string; toolName: string }) {
374
+ if (!isRecord(value) || !Array.isArray(value.tools)) {
375
+ throw new Error("Chrome sent a malformed WebMCP tool-change event");
376
+ }
377
+ return value.tools.some((tool) => {
378
+ if (!isRecord(tool) || typeof tool.frameId !== "string" || typeof tool.name !== "string") {
379
+ throw new Error("Chrome sent a malformed WebMCP tool-change event");
380
+ }
381
+ return tool.frameId === expected.frameId && tool.name === expected.toolName;
382
+ });
396
383
  }
397
384
 
398
385
  function parseInvocationId(value: unknown) {
399
- return isRecord(value) && typeof value.invocationId === "string" ? value.invocationId : undefined;
386
+ return isRecord(value) && typeof value.invocationId === "string" ? value.invocationId : undefined;
400
387
  }
401
388
 
402
389
  function parseToolResponded(value: unknown): WebMcpInvocationResponse | undefined {
403
- if (
404
- !isRecord(value) ||
405
- typeof value.invocationId !== "string" ||
406
- !(["Canceled", "Completed", "Error"] as const).includes(value.status as never) ||
407
- (value.errorText !== undefined && typeof value.errorText !== "string")
408
- ) {
409
- return undefined;
410
- }
411
- return {
412
- invocationId: value.invocationId,
413
- status: value.status as WebMcpInvocationResponse["status"],
414
- ...(value.output === undefined ? {} : { output: value.output }),
415
- ...(value.errorText === undefined ? {} : { errorText: value.errorText }),
416
- ...(value.exception === undefined ? {} : { exception: value.exception }),
417
- };
390
+ if (
391
+ !isRecord(value) ||
392
+ typeof value.invocationId !== "string" ||
393
+ !(["Canceled", "Completed", "Error"] as const).includes(value.status as never) ||
394
+ (value.errorText !== undefined && typeof value.errorText !== "string")
395
+ ) {
396
+ return undefined;
397
+ }
398
+ return {
399
+ invocationId: value.invocationId,
400
+ status: value.status as WebMcpInvocationResponse["status"],
401
+ ...(value.output === undefined ? {} : { output: value.output }),
402
+ ...(value.errorText === undefined ? {} : { errorText: value.errorText }),
403
+ ...(value.exception === undefined ? {} : { exception: value.exception }),
404
+ };
418
405
  }
419
406
 
420
407
  function withDeadline(signal: AbortSignal) {
421
- return AbortSignal.any([signal, AbortSignal.timeout(DEFAULT_TIMEOUT_MS)]);
408
+ return AbortSignal.any([signal, AbortSignal.timeout(DEFAULT_TIMEOUT_MS)]);
422
409
  }
423
410
 
424
411
  function isRecord(value: unknown): value is Record<string, unknown> {
425
- return typeof value === "object" && value !== null && !Array.isArray(value);
412
+ return typeof value === "object" && value !== null && !Array.isArray(value);
426
413
  }