@mehmoodqureshi/chrome-mcp 0.9.1 → 0.9.3
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 +44 -2
- package/dist/shared/auth-wall.d.ts +51 -0
- package/dist/shared/auth-wall.js +140 -0
- package/dist/shared/page-fns.js +208 -182
- package/dist/shared/screenshot.d.ts +14 -0
- package/dist/shared/screenshot.js +19 -4
- package/dist/shared/snapshot.d.ts +24 -2
- package/dist/shared/snapshot.js +117 -33
- package/dist/src/bridge/connection.d.ts +13 -0
- package/dist/src/bridge/connection.js +55 -1
- package/dist/src/bridge/server.d.ts +2 -0
- package/dist/src/bridge/server.js +5 -0
- package/dist/src/bridge/workspace.d.ts +1 -1
- package/dist/src/bridge/workspace.js +2 -2
- package/dist/src/config.js +7 -0
- package/dist/src/executor/cdp-executor.d.ts +3 -2
- package/dist/src/executor/cdp-executor.js +21 -6
- package/dist/src/executor/extension-executor.d.ts +6 -2
- package/dist/src/executor/extension-executor.js +18 -5
- package/dist/src/executor/stub-executor.d.ts +15 -0
- package/dist/src/executor/stub-executor.js +26 -0
- package/dist/src/executor/types.d.ts +28 -3
- package/dist/src/mcp/locate.d.ts +7 -4
- package/dist/src/mcp/locate.js +15 -8
- package/dist/src/mcp/tools.js +137 -40
- package/dist/src/security/policy.d.ts +4 -0
- package/dist/src/security/policy.js +2 -0
- package/docs/BLUEPRINT.md +2 -2
- package/extension-dist/background.js +494 -263
- package/extension-dist/manifest.json +3 -2
- package/package.json +1 -1
|
@@ -18,10 +18,11 @@ const workspace_1 = require("../bridge/workspace");
|
|
|
18
18
|
* Deliberately short. It exists to cover back-to-back calls (a `batch`, or an
|
|
19
19
|
* agent's read → click → read), where the tab demonstrably has not changed
|
|
20
20
|
* between them. Past that, pay the round-trip. Note the extension re-gates every
|
|
21
|
-
* command against the tab's live URL regardless, so
|
|
22
|
-
* pre-check precision for half the traffic — never
|
|
21
|
+
* command against the tab's live URL regardless (fail-closed, authoritative), so
|
|
22
|
+
* this window trades a little pre-check precision for half the traffic — never
|
|
23
|
+
* enforcement itself.
|
|
23
24
|
*/
|
|
24
|
-
const ACTIVE_URL_TTL_MS =
|
|
25
|
+
const ACTIVE_URL_TTL_MS = 2_000;
|
|
25
26
|
/** Flatten frame options into the params a wire command carries. */
|
|
26
27
|
function frameParams(o) {
|
|
27
28
|
if (!o)
|
|
@@ -88,6 +89,11 @@ class ExtensionExecutor {
|
|
|
88
89
|
cachedActiveUrl() {
|
|
89
90
|
return this.bridge.lastActiveUrl(this.activeProfile(), ACTIVE_URL_TTL_MS);
|
|
90
91
|
}
|
|
92
|
+
/** A specific tab's URL as last reported (by a result for that tab, or by a
|
|
93
|
+
* `tabs_list`), if fresh enough to gate against. */
|
|
94
|
+
cachedTabUrl(tabId) {
|
|
95
|
+
return this.bridge.lastTabUrl(this.activeProfile(), tabId, ACTIVE_URL_TTL_MS);
|
|
96
|
+
}
|
|
91
97
|
// -- tabs ---------------------------------------------------------------
|
|
92
98
|
async tabsList() {
|
|
93
99
|
return (await this.send('tabs_list', {}));
|
|
@@ -145,7 +151,7 @@ class ExtensionExecutor {
|
|
|
145
151
|
return (await this.send('get_html', { ...targetParams(t), ...frameParams(opts), outer: opts?.outer }, { tabId: opts?.tabId }));
|
|
146
152
|
}
|
|
147
153
|
async snapshot(opts) {
|
|
148
|
-
return (await this.send('snapshot', { interactiveOnly: opts?.interactiveOnly, max: opts?.max, ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
154
|
+
return (await this.send('snapshot', { interactiveOnly: opts?.interactiveOnly, max: opts?.max, locator: opts?.locator, ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
149
155
|
}
|
|
150
156
|
async getCookies(opts) {
|
|
151
157
|
return (await this.send('get_cookies', { url: opts?.url }, { tabId: opts?.tabId }));
|
|
@@ -154,7 +160,14 @@ class ExtensionExecutor {
|
|
|
154
160
|
return (await this.send('storage', { op: args.op, key: args.key, value: args.value, session: args.session }, { tabId: args.tabId }));
|
|
155
161
|
}
|
|
156
162
|
async screenshot(opts) {
|
|
157
|
-
return (await this.send('screenshot', {
|
|
163
|
+
return (await this.send('screenshot', {
|
|
164
|
+
fullPage: opts?.fullPage,
|
|
165
|
+
format: opts?.format,
|
|
166
|
+
quality: opts?.quality,
|
|
167
|
+
scale: opts?.scale,
|
|
168
|
+
...targetParams(opts?.target),
|
|
169
|
+
...frameParams(opts),
|
|
170
|
+
}, { tabId: opts?.tabId }));
|
|
158
171
|
}
|
|
159
172
|
async eval(expression, opts) {
|
|
160
173
|
const result = (await this.send('eval', { expression, awaitPromise: opts?.awaitPromise, ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
@@ -54,6 +54,15 @@ export interface StubOptions {
|
|
|
54
54
|
snapshotNodes?: SnapshotNode[];
|
|
55
55
|
/** Frames the stub reports for `frames_list`. */
|
|
56
56
|
frames?: FrameInfo[];
|
|
57
|
+
/** Applied after any action or history move: models a click/submit/back that
|
|
58
|
+
* lands the tab on a different page (e.g. a redirect to a sign-in wall). */
|
|
59
|
+
afterAction?: {
|
|
60
|
+
url?: string;
|
|
61
|
+
nodes?: SnapshotNode[];
|
|
62
|
+
};
|
|
63
|
+
/** When true, `waitFor` rejects with TIMEOUT - the error a wait on a page
|
|
64
|
+
* that silently became a login form used to surface. */
|
|
65
|
+
waitForTimesOut?: boolean;
|
|
57
66
|
/** What the in-page observers return. Absent = the hook is not installed,
|
|
58
67
|
* which is the case the tools must report clearly rather than as an empty list. */
|
|
59
68
|
observers?: ObserverReadResult;
|
|
@@ -76,6 +85,10 @@ export declare class StubExecutor implements Executor {
|
|
|
76
85
|
snapshotNodes: SnapshotNode[];
|
|
77
86
|
private readonly frames;
|
|
78
87
|
private readonly observerState?;
|
|
88
|
+
private readonly afterAction?;
|
|
89
|
+
private readonly waitForTimesOut;
|
|
90
|
+
/** How many snapshots were taken - the auth guard must cost none when off. */
|
|
91
|
+
snapshotCalls: number;
|
|
79
92
|
/** The last observer args received, so a test can assert what was requested. */
|
|
80
93
|
lastObserverArgs?: ObserverArgs;
|
|
81
94
|
/** How many times the gate actually asked for the tab list — the round-trip
|
|
@@ -110,6 +123,8 @@ export declare class StubExecutor implements Executor {
|
|
|
110
123
|
back(): Promise<NavResult>;
|
|
111
124
|
forward(): Promise<NavResult>;
|
|
112
125
|
reload(): Promise<NavResult>;
|
|
126
|
+
/** Move the stub tab to the configured post-action page, if any. */
|
|
127
|
+
private landed;
|
|
113
128
|
click(): Promise<ActionOk>;
|
|
114
129
|
type(): Promise<ActionOk>;
|
|
115
130
|
fill(): Promise<ActionOk>;
|
|
@@ -32,6 +32,10 @@ class StubExecutor {
|
|
|
32
32
|
snapshotNodes;
|
|
33
33
|
frames;
|
|
34
34
|
observerState;
|
|
35
|
+
afterAction;
|
|
36
|
+
waitForTimesOut;
|
|
37
|
+
/** How many snapshots were taken - the auth guard must cost none when off. */
|
|
38
|
+
snapshotCalls = 0;
|
|
35
39
|
/** The last observer args received, so a test can assert what was requested. */
|
|
36
40
|
lastObserverArgs;
|
|
37
41
|
/** How many times the gate actually asked for the tab list — the round-trip
|
|
@@ -40,6 +44,8 @@ class StubExecutor {
|
|
|
40
44
|
ready = false;
|
|
41
45
|
constructor(opts = {}) {
|
|
42
46
|
this.url = opts.activeUrl ?? 'about:blank';
|
|
47
|
+
this.afterAction = opts.afterAction;
|
|
48
|
+
this.waitForTimesOut = opts.waitForTimesOut ?? false;
|
|
43
49
|
this.evalThrows = opts.evalThrows ?? false;
|
|
44
50
|
this.tabsListThrows = opts.tabsListThrows ?? false;
|
|
45
51
|
this.noTabs = opts.noTabs ?? false;
|
|
@@ -133,15 +139,28 @@ class StubExecutor {
|
|
|
133
139
|
return { url: args.url, title: 'Stub Page', httpStatus: 200 };
|
|
134
140
|
}
|
|
135
141
|
async back() {
|
|
142
|
+
this.landed();
|
|
136
143
|
return { url: this.url, title: 'Stub Page' };
|
|
137
144
|
}
|
|
138
145
|
async forward() {
|
|
146
|
+
this.landed();
|
|
139
147
|
return { url: this.url, title: 'Stub Page' };
|
|
140
148
|
}
|
|
141
149
|
async reload() {
|
|
150
|
+
this.landed();
|
|
142
151
|
return { url: this.url, title: 'Stub Page' };
|
|
143
152
|
}
|
|
153
|
+
/** Move the stub tab to the configured post-action page, if any. */
|
|
154
|
+
landed() {
|
|
155
|
+
if (!this.afterAction)
|
|
156
|
+
return;
|
|
157
|
+
if (this.afterAction.url !== undefined)
|
|
158
|
+
this.url = this.afterAction.url;
|
|
159
|
+
if (this.afterAction.nodes !== undefined)
|
|
160
|
+
this.snapshotNodes = this.afterAction.nodes;
|
|
161
|
+
}
|
|
144
162
|
async click() {
|
|
163
|
+
this.landed();
|
|
145
164
|
return ok;
|
|
146
165
|
}
|
|
147
166
|
async type() {
|
|
@@ -149,18 +168,22 @@ class StubExecutor {
|
|
|
149
168
|
this.remainingWriteDisconnects--;
|
|
150
169
|
throw new types_1.ExecutorError('EXTENSION_DISCONNECTED', 'stub: service worker recycled mid-command');
|
|
151
170
|
}
|
|
171
|
+
this.landed();
|
|
152
172
|
return ok;
|
|
153
173
|
}
|
|
154
174
|
async fill() {
|
|
175
|
+
this.landed();
|
|
155
176
|
return ok;
|
|
156
177
|
}
|
|
157
178
|
async press() {
|
|
179
|
+
this.landed();
|
|
158
180
|
return ok;
|
|
159
181
|
}
|
|
160
182
|
async hover() {
|
|
161
183
|
return ok;
|
|
162
184
|
}
|
|
163
185
|
async selectOption() {
|
|
186
|
+
this.landed();
|
|
164
187
|
return ok;
|
|
165
188
|
}
|
|
166
189
|
async scroll() {
|
|
@@ -175,6 +198,7 @@ class StubExecutor {
|
|
|
175
198
|
return { html: this.htmlPayload };
|
|
176
199
|
}
|
|
177
200
|
async snapshot() {
|
|
201
|
+
this.snapshotCalls++;
|
|
178
202
|
return { url: this.url, title: 'Stub Page', nodes: this.snapshotNodes, truncated: false };
|
|
179
203
|
}
|
|
180
204
|
async getCookies() {
|
|
@@ -195,6 +219,8 @@ class StubExecutor {
|
|
|
195
219
|
return { ok: true, value: 'stub-value', type: 'string' };
|
|
196
220
|
}
|
|
197
221
|
async waitFor() {
|
|
222
|
+
if (this.waitForTimesOut)
|
|
223
|
+
throw new types_1.ExecutorError('TIMEOUT', 'stub: wait_for timed out');
|
|
198
224
|
return { matched: true, waitedMs: 0 };
|
|
199
225
|
}
|
|
200
226
|
async download(args) {
|
|
@@ -111,9 +111,18 @@ export interface WaitResult {
|
|
|
111
111
|
export interface ActionOk {
|
|
112
112
|
ok: true;
|
|
113
113
|
}
|
|
114
|
+
export type ScreenshotFormat = 'png' | 'jpeg';
|
|
115
|
+
/** Encoding knobs every backend accepts. All optional; see shared/screenshot.ts for defaults. */
|
|
116
|
+
export interface ScreenshotEncoding {
|
|
117
|
+
format?: ScreenshotFormat;
|
|
118
|
+
/** JPEG only, 1-100. */
|
|
119
|
+
quality?: number;
|
|
120
|
+
/** Output pixels per CSS pixel (1 = CSS size, 2 = device pixels on a Retina display). */
|
|
121
|
+
scale?: number;
|
|
122
|
+
}
|
|
114
123
|
export interface ScreenshotResult {
|
|
115
124
|
dataBase64: string;
|
|
116
|
-
mimeType: 'image/png';
|
|
125
|
+
mimeType: 'image/png' | 'image/jpeg';
|
|
117
126
|
width: number;
|
|
118
127
|
height: number;
|
|
119
128
|
/** fullPage capture exceeded the height cap; `fullHeight` reports the real size. */
|
|
@@ -153,6 +162,13 @@ export interface SnapshotResult {
|
|
|
153
162
|
title: string;
|
|
154
163
|
nodes: SnapshotNode[];
|
|
155
164
|
truncated: boolean;
|
|
165
|
+
/** Locator mode, no match: what the page had of that role (for the error message). */
|
|
166
|
+
nearby?: string[];
|
|
167
|
+
}
|
|
168
|
+
/** A role/name query the page resolves itself (see shared/snapshot.ts). */
|
|
169
|
+
export interface SnapshotLocator {
|
|
170
|
+
role?: string;
|
|
171
|
+
name?: string;
|
|
156
172
|
}
|
|
157
173
|
export interface CookieItem {
|
|
158
174
|
name: string;
|
|
@@ -205,6 +221,12 @@ export interface Executor {
|
|
|
205
221
|
* report cheaply simply omit it.
|
|
206
222
|
*/
|
|
207
223
|
cachedActiveUrl?(): string | null;
|
|
224
|
+
/**
|
|
225
|
+
* Same idea for an explicitly-targeted tab: its URL if the backend already
|
|
226
|
+
* knows it recently enough (the extension reports it on every result for that
|
|
227
|
+
* tab, and a `tabsList` reports it for every tab). Null → resolve properly.
|
|
228
|
+
*/
|
|
229
|
+
cachedTabUrl?(tabId: TabId): string | null;
|
|
208
230
|
tabsList(): Promise<TabInfo[]>;
|
|
209
231
|
tabSelect(tabId: TabId): Promise<TabInfo>;
|
|
210
232
|
/** Open a tab. `active` (default true) focuses it; pass false to open in the background. */
|
|
@@ -279,6 +301,7 @@ export interface Executor {
|
|
|
279
301
|
tabId?: TabId;
|
|
280
302
|
interactiveOnly?: boolean;
|
|
281
303
|
max?: number;
|
|
304
|
+
locator?: SnapshotLocator;
|
|
282
305
|
} & FrameOpts): Promise<SnapshotResult>;
|
|
283
306
|
/** Read cookies visible to the active tab's URL (or a given url). */
|
|
284
307
|
getCookies(opts?: {
|
|
@@ -299,7 +322,7 @@ export interface Executor {
|
|
|
299
322
|
tabId?: TabId;
|
|
300
323
|
fullPage?: boolean;
|
|
301
324
|
target?: Target;
|
|
302
|
-
} & FrameOpts): Promise<ScreenshotResult>;
|
|
325
|
+
} & ScreenshotEncoding & FrameOpts): Promise<ScreenshotResult>;
|
|
303
326
|
eval(expression: string, opts?: {
|
|
304
327
|
tabId?: TabId;
|
|
305
328
|
awaitPromise?: boolean;
|
|
@@ -349,7 +372,9 @@ export interface Executor {
|
|
|
349
372
|
* (which only carries codes that originate inside the extension); these extra
|
|
350
373
|
* codes describe failures on the server half (no backend, launch failed, etc.).
|
|
351
374
|
*/
|
|
352
|
-
export type ExecutorErrorCodeLocal = 'NO_BACKEND' | 'EXTENSION_DISCONNECTED' | 'TIMEOUT' | 'TAB_NOT_FOUND' | 'STALE_TAB' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_FAILED' | 'LAUNCH_FAILED' | 'DETACHED' | 'TARGET_GONE' | 'POLICY_DENIED' | 'DEVTOOLS_OPEN' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'FRAME_NOT_FOUND' | 'OBSERVERS_DISABLED' | 'UNSUPPORTED' | 'BACKPRESSURE'
|
|
375
|
+
export type ExecutorErrorCodeLocal = 'NO_BACKEND' | 'EXTENSION_DISCONNECTED' | 'TIMEOUT' | 'TAB_NOT_FOUND' | 'STALE_TAB' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_FAILED' | 'LAUNCH_FAILED' | 'DETACHED' | 'TARGET_GONE' | 'POLICY_DENIED' | 'DEVTOOLS_OPEN' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'FRAME_NOT_FOUND' | 'OBSERVERS_DISABLED' | 'UNSUPPORTED' | 'BACKPRESSURE'
|
|
376
|
+
/** The page is a sign-in wall (session expired mid-run). Raised only when the caller opts in via `failOnAuthWall`. */
|
|
377
|
+
| 'AUTH_REQUIRED';
|
|
353
378
|
export declare class ExecutorError extends Error {
|
|
354
379
|
readonly code: ExecutorErrorCodeLocal;
|
|
355
380
|
constructor(code: ExecutorErrorCodeLocal, message: string);
|
package/dist/src/mcp/locate.d.ts
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* redeploy.
|
|
10
10
|
*
|
|
11
11
|
* A locator closes that: `{ role: 'button', name: 'Sign in' }` resolves through
|
|
12
|
-
* one snapshot
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* one snapshot and the caller never sees the tree. The page does the matching
|
|
13
|
+
* itself (`collectSnapshot` with a locator returns only the strongest-tier
|
|
14
|
+
* hits, and stamps refs on those alone), so what crosses the bridge is a
|
|
15
|
+
* handful of nodes rather than 400; this module re-scores them — same tiers:
|
|
16
|
+
* exact, then case-insensitive, then prefix, then contains — so an unambiguous
|
|
17
|
+
* name wins outright and an ambiguous one fails loudly with the candidates
|
|
18
|
+
* rather than silently clicking the first row.
|
|
16
19
|
*/
|
|
17
20
|
import type { Executor, SnapshotNode, Target } from '../executor/types';
|
|
18
21
|
export interface Locator {
|
package/dist/src/mcp/locate.js
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
* redeploy.
|
|
11
11
|
*
|
|
12
12
|
* A locator closes that: `{ role: 'button', name: 'Sign in' }` resolves through
|
|
13
|
-
* one snapshot
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* one snapshot and the caller never sees the tree. The page does the matching
|
|
14
|
+
* itself (`collectSnapshot` with a locator returns only the strongest-tier
|
|
15
|
+
* hits, and stamps refs on those alone), so what crosses the bridge is a
|
|
16
|
+
* handful of nodes rather than 400; this module re-scores them — same tiers:
|
|
17
|
+
* exact, then case-insensitive, then prefix, then contains — so an unambiguous
|
|
18
|
+
* name wins outright and an ambiguous one fails loudly with the candidates
|
|
19
|
+
* rather than silently clicking the first row.
|
|
17
20
|
*/
|
|
18
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
22
|
exports.hasLocator = hasLocator;
|
|
@@ -60,6 +63,7 @@ async function resolveLocator(ex, loc, opts = {}) {
|
|
|
60
63
|
tabId: opts.tabId,
|
|
61
64
|
interactiveOnly: false,
|
|
62
65
|
max: 400,
|
|
66
|
+
locator: want,
|
|
63
67
|
frameId: opts.frameId,
|
|
64
68
|
allFrames: opts.allFrames,
|
|
65
69
|
});
|
|
@@ -79,10 +83,13 @@ async function resolveLocator(ex, loc, opts = {}) {
|
|
|
79
83
|
}
|
|
80
84
|
const describe = (n) => `${n.role} "${n.name}"`;
|
|
81
85
|
if (winners.length === 0) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
+
// A page that scored in place reports what it had of that role as `nearby`;
|
|
87
|
+
// a backend that returned the full tree leaves it to us.
|
|
88
|
+
const sample = snap.nearby ??
|
|
89
|
+
snap.nodes
|
|
90
|
+
.filter((n) => !want.role || norm(n.role) === norm(want.role))
|
|
91
|
+
.slice(0, 8)
|
|
92
|
+
.map(describe);
|
|
86
93
|
throw new validators_1.McpToolError(`no element matches ${JSON.stringify(want)}. ` +
|
|
87
94
|
(sample.length
|
|
88
95
|
? `Closest by role: ${sample.join(', ')}. `
|