@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/discovery.ts
CHANGED
|
@@ -1,235 +1,231 @@
|
|
|
1
1
|
import { getPage } from "../cdp-client.js";
|
|
2
2
|
import { type DevToolsPage, webMcpOperationIsCurrent } from "../runtime.js";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
MAX_WEBMCP_TOOLS,
|
|
5
|
+
normalizeWebMcpOutput,
|
|
6
|
+
normalizeWebMcpTool,
|
|
7
|
+
requireMatchingWebMcpTool,
|
|
8
|
+
type WebMcpToolDescriptor,
|
|
9
|
+
type WebMcpToolIdentity,
|
|
10
|
+
webMcpErrorMessage,
|
|
11
11
|
} from "./policy.js";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
cancelWebMcpInvocation,
|
|
14
|
+
connectWebMcpPage,
|
|
15
|
+
disableWebMcp,
|
|
16
|
+
enableWebMcp,
|
|
17
|
+
enableWebMcpIdentityTracking,
|
|
18
|
+
invokeWebMcpTool,
|
|
19
|
+
readWebMcpFrames,
|
|
20
|
+
requireWebMcpDomain,
|
|
21
|
+
type WebMcpFrame,
|
|
22
|
+
type WebMcpProtocolTool,
|
|
23
|
+
watchWebMcpIdentity,
|
|
24
24
|
} from "./protocol.js";
|
|
25
25
|
|
|
26
26
|
export interface WebMcpOperationIdentity {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
owner: object;
|
|
28
|
+
sessionGeneration: number;
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
webMcpGeneration: number;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface WebMcpInvocationResult {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
invocationId: string;
|
|
35
|
+
output: unknown;
|
|
36
|
+
tool: WebMcpToolDescriptor;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export async function discoverWebMcpTools(page: DevToolsPage, operation: WebMcpOperationIdentity) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
assertOperationCurrent(operation);
|
|
41
|
+
await requireWebMcpDomain(operation.signal, operation.owner);
|
|
42
|
+
assertOperationCurrent(operation);
|
|
43
|
+
const client = await connectWebMcpPage(page, operation.signal);
|
|
44
|
+
try {
|
|
45
|
+
const tools = await readInventory(client, page, operation);
|
|
46
|
+
await requireStablePage(client, page, tools, operation);
|
|
47
|
+
return tools;
|
|
48
|
+
} finally {
|
|
49
|
+
await disableWebMcp(client);
|
|
50
|
+
client.close();
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export async function invokeDiscoveredWebMcpTool(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
page: DevToolsPage,
|
|
56
|
+
expected: WebMcpToolIdentity,
|
|
57
|
+
input: Record<string, unknown>,
|
|
58
|
+
operation: WebMcpOperationIdentity,
|
|
59
59
|
): Promise<WebMcpInvocationResult> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
60
|
+
assertOperationCurrent(operation);
|
|
61
|
+
await requireWebMcpDomain(operation.signal, operation.owner);
|
|
62
|
+
assertOperationCurrent(operation);
|
|
63
|
+
const client = await connectWebMcpPage(page, operation.signal);
|
|
64
|
+
try {
|
|
65
|
+
await enableWebMcpIdentityTracking(client, operation.signal);
|
|
66
|
+
assertOperationCurrent(operation);
|
|
67
|
+
const tools = await readInventory(client, page, operation);
|
|
68
|
+
const tool = requireMatchingWebMcpTool(tools, expected);
|
|
69
|
+
const identityWatch = watchWebMcpIdentity(
|
|
70
|
+
client,
|
|
71
|
+
{ documentId: tool.documentId, frameId: tool.frameId, toolName: tool.name },
|
|
72
|
+
operation.signal,
|
|
73
|
+
);
|
|
74
|
+
const guardedOperation = { ...operation, signal: identityWatch.signal };
|
|
75
|
+
try {
|
|
76
|
+
await requireStablePage(client, page, [tool], guardedOperation);
|
|
77
|
+
assertOperationCurrent(guardedOperation);
|
|
78
|
+
const response = await invokeWebMcpTool(
|
|
79
|
+
client,
|
|
80
|
+
{ frameId: tool.frameId, toolName: tool.name, input },
|
|
81
|
+
guardedOperation.signal,
|
|
82
|
+
(invocationId) => cancelWebMcpInvocation(page, invocationId),
|
|
83
|
+
);
|
|
84
|
+
assertOperationCurrent(guardedOperation);
|
|
85
|
+
if (response.status === "Canceled") {
|
|
86
|
+
throw new DOMException("Chrome canceled the WebMCP tool invocation.", "AbortError");
|
|
87
|
+
}
|
|
88
|
+
if (response.status === "Error") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`WebMCP tool failed: ${webMcpErrorMessage(response.errorText ?? remoteExceptionText(response.exception))}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
invocationId: response.invocationId,
|
|
95
|
+
output: normalizeWebMcpOutput(response.output ?? null),
|
|
96
|
+
tool,
|
|
97
|
+
};
|
|
98
|
+
} finally {
|
|
99
|
+
await identityWatch.dispose();
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
await disableWebMcp(client);
|
|
103
|
+
client.close();
|
|
104
|
+
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
async function readInventory(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
client: Awaited<ReturnType<typeof connectWebMcpPage>>,
|
|
109
|
+
page: DevToolsPage,
|
|
110
|
+
operation: WebMcpOperationIdentity,
|
|
111
111
|
) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
const framesBefore = await readWebMcpFrames(client, operation.signal);
|
|
113
|
+
assertOperationCurrent(operation);
|
|
114
|
+
const protocolTools = await enableWebMcp(client, operation.signal);
|
|
115
|
+
assertOperationCurrent(operation);
|
|
116
|
+
if (protocolTools.length > MAX_WEBMCP_TOOLS) {
|
|
117
|
+
throw new Error(`WebMCP page exposes more than ${MAX_WEBMCP_TOOLS} tools.`);
|
|
118
|
+
}
|
|
119
|
+
const framesAfter = await readWebMcpFrames(client, operation.signal);
|
|
120
|
+
assertOperationCurrent(operation);
|
|
121
|
+
requireStableInventoryDocuments(protocolTools, framesBefore, framesAfter);
|
|
122
|
+
return normalizeInventory(protocolTools, framesAfter, page, operation);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function requireStableInventoryDocuments(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
tools: readonly WebMcpProtocolTool[],
|
|
127
|
+
before: readonly WebMcpFrame[],
|
|
128
|
+
after: readonly WebMcpFrame[],
|
|
129
129
|
) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
}
|
|
130
|
+
const beforeLoaders = new Map(before.map((frame) => [frame.id, frame.loaderId]));
|
|
131
|
+
const afterLoaders = new Map(after.map((frame) => [frame.id, frame.loaderId]));
|
|
132
|
+
for (const frameId of new Set(tools.map((tool) => tool.frameId))) {
|
|
133
|
+
const beforeLoader = beforeLoaders.get(frameId);
|
|
134
|
+
const afterLoader = afterLoaders.get(frameId);
|
|
135
|
+
if (!beforeLoader || !afterLoader || beforeLoader !== afterLoader) {
|
|
136
|
+
throw new Error("A WebMCP frame document changed while Chrome was publishing its tool inventory.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
function normalizeInventory(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
protocolTools: readonly WebMcpProtocolTool[],
|
|
143
|
+
frames: readonly WebMcpFrame[],
|
|
144
|
+
page: DevToolsPage,
|
|
145
|
+
operation: WebMcpOperationIdentity,
|
|
148
146
|
) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
147
|
+
const frameDocuments = new Map(
|
|
148
|
+
frames.map((frame) => [frame.id, { documentId: frame.loaderId, origin: frameOrigin(frame) }]),
|
|
149
|
+
);
|
|
150
|
+
const tools = protocolTools.map((tool) => {
|
|
151
|
+
const frame = frameDocuments.get(tool.frameId);
|
|
152
|
+
if (!frame) {
|
|
153
|
+
throw new Error(`WebMCP tool ${webMcpErrorMessage(tool.name)} belongs to an unknown frame.`);
|
|
154
|
+
}
|
|
155
|
+
return normalizeWebMcpTool(tool, {
|
|
156
|
+
documentId: frame.documentId,
|
|
157
|
+
frameOrigin: frame.origin,
|
|
158
|
+
pageId: page.id,
|
|
159
|
+
pageUrl: page.url,
|
|
160
|
+
sessionGeneration: generationToken(operation),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
const identities = new Set<string>();
|
|
164
|
+
for (const tool of tools) {
|
|
165
|
+
const key = `${tool.frameId}\u0000${tool.name}`;
|
|
166
|
+
if (identities.has(key)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`WebMCP tool identity is duplicated: ${webMcpErrorMessage(tool.name)} in frame ${webMcpErrorMessage(tool.frameId)}.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
identities.add(key);
|
|
172
|
+
}
|
|
173
|
+
return tools;
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
async function requireStablePage(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
client: Awaited<ReturnType<typeof connectWebMcpPage>>,
|
|
178
|
+
page: DevToolsPage,
|
|
179
|
+
tools: readonly WebMcpToolDescriptor[],
|
|
180
|
+
operation: WebMcpOperationIdentity,
|
|
183
181
|
) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
182
|
+
const current = await getPage(page.id, {
|
|
183
|
+
sessionOwner: operation.owner,
|
|
184
|
+
signal: operation.signal,
|
|
185
|
+
});
|
|
186
|
+
assertOperationCurrent(operation);
|
|
187
|
+
if (
|
|
188
|
+
current.url !== page.url ||
|
|
189
|
+
current.webSocketDebuggerUrl !== page.webSocketDebuggerUrl ||
|
|
190
|
+
current.type !== page.type
|
|
191
|
+
) {
|
|
192
|
+
throw new Error("The Chrome page navigated or was replaced during the WebMCP operation.");
|
|
193
|
+
}
|
|
194
|
+
const finalFrames = await readWebMcpFrames(client, operation.signal);
|
|
195
|
+
assertOperationCurrent(operation);
|
|
196
|
+
const finalDocuments = new Map(finalFrames.map((frame) => [frame.id, frame.loaderId]));
|
|
197
|
+
for (const tool of tools) {
|
|
198
|
+
if (finalDocuments.get(tool.frameId) !== tool.documentId) {
|
|
199
|
+
throw new Error("A WebMCP frame document changed during the final page identity check.");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
function assertOperationCurrent(operation: WebMcpOperationIdentity) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
operation.signal.throwIfAborted();
|
|
206
|
+
if (!webMcpOperationIsCurrent(operation)) {
|
|
207
|
+
throw new DOMException("The WebMCP operation became stale.", "AbortError");
|
|
208
|
+
}
|
|
211
209
|
}
|
|
212
210
|
|
|
213
211
|
function generationToken(operation: WebMcpOperationIdentity) {
|
|
214
|
-
|
|
212
|
+
return `${operation.sessionGeneration}:${operation.webMcpGeneration}`;
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
function frameOrigin(frame: WebMcpFrame) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
225
|
-
}
|
|
216
|
+
if (frame.securityOrigin && frame.securityOrigin !== "://") return frame.securityOrigin;
|
|
217
|
+
try {
|
|
218
|
+
return new URL(frame.url).origin;
|
|
219
|
+
} catch {
|
|
220
|
+
throw new Error(`Chrome returned an invalid WebMCP frame URL: ${webMcpErrorMessage(frame.url)}`);
|
|
221
|
+
}
|
|
226
222
|
}
|
|
227
223
|
|
|
228
224
|
function remoteExceptionText(value: unknown) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
225
|
+
if (typeof value === "object" && value !== null) {
|
|
226
|
+
const exception = value as { description?: unknown; value?: unknown };
|
|
227
|
+
if (typeof exception.description === "string") return exception.description;
|
|
228
|
+
if (typeof exception.value === "string") return exception.value;
|
|
229
|
+
}
|
|
230
|
+
return "The page reported an unknown tool error.";
|
|
235
231
|
}
|