@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.
- package/README.md +1 -0
- package/dist/chunks/{browser-settings-menu-IMB6HXSU.js → browser-settings-menu-FVMCCUPM.ts} +16 -42
- package/dist/chunks/browser-settings-menu-FVMCCUPM.ts.map +7 -0
- package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4B3EQHOH.ts} +2 -2
- package/dist/chunks/chunk-4B3EQHOH.ts.map +7 -0
- package/dist/chunks/{chunk-SRT3UUIP.js → chunk-LSJSDY2M.ts} +23 -74
- package/dist/chunks/chunk-LSJSDY2M.ts.map +7 -0
- package/dist/chunks/{chunk-LLXOCRNF.js → chunk-V7N3WI5B.ts} +12 -33
- package/dist/chunks/chunk-V7N3WI5B.ts.map +7 -0
- package/dist/chunks/{menu-UJETYMSB.js → menu-MTPSLDHZ.ts} +10 -23
- package/dist/chunks/menu-MTPSLDHZ.ts.map +7 -0
- package/dist/chunks/{tools-6P7EUX37.js → tools-OVNH45IJ.ts} +17 -59
- package/dist/chunks/tools-OVNH45IJ.ts.map +7 -0
- package/dist/index.ts +18 -42
- package/dist/index.ts.map +2 -2
- package/package.json +54 -55
- package/src/browser-manager.ts +872 -942
- package/src/browser-settings-menu.ts +315 -350
- package/src/cdp-client.ts +381 -398
- package/src/chrome-devtools.ts +205 -232
- package/src/display.ts +26 -29
- package/src/lazy-tools.ts +158 -181
- package/src/menu.ts +409 -446
- package/src/render.ts +72 -81
- package/src/runtime.ts +137 -140
- package/src/screenshot.ts +170 -186
- package/src/settings.ts +472 -523
- package/src/tool-names.ts +8 -14
- package/src/tool-selector.ts +242 -271
- package/src/tools.ts +218 -226
- package/src/webmcp/discovery.ts +184 -188
- package/src/webmcp/policy.ts +296 -329
- package/src/webmcp/protocol.ts +324 -337
- package/src/webmcp/tools.ts +136 -147
- package/dist/chunks/browser-settings-menu-IMB6HXSU.js.map +0 -7
- package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
- package/dist/chunks/chunk-LLXOCRNF.js.map +0 -7
- package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
- package/dist/chunks/menu-UJETYMSB.js.map +0 -7
- package/dist/chunks/tools-6P7EUX37.js.map +0 -7
package/src/webmcp/protocol.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
autosubmit?: boolean;
|
|
7
|
+
consequential?: boolean;
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
untrustedContent?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface WebMcpProtocolTool {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
id: string;
|
|
24
|
+
loaderId: string;
|
|
25
|
+
securityOrigin?: string;
|
|
26
|
+
url: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface WebMcpInvocationResponse {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
38
|
+
dispose(): Promise<void>;
|
|
39
|
+
signal: AbortSignal;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
interface ProtocolDescription {
|
|
43
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
client: CdpClient,
|
|
146
|
+
expected: { documentId: string; frameId: string; toolName: string },
|
|
147
|
+
signal: AbortSignal,
|
|
152
148
|
): WebMcpIdentityWatch {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
386
|
+
return isRecord(value) && typeof value.invocationId === "string" ? value.invocationId : undefined;
|
|
400
387
|
}
|
|
401
388
|
|
|
402
389
|
function parseToolResponded(value: unknown): WebMcpInvocationResponse | undefined {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
426
413
|
}
|