@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
package/dist/src/mcp/tools.js
CHANGED
|
@@ -29,6 +29,7 @@ const helpers_1 = require("./helpers");
|
|
|
29
29
|
const redact_1 = require("./redact");
|
|
30
30
|
const locate_1 = require("./locate");
|
|
31
31
|
const snapdiff_1 = require("./snapdiff");
|
|
32
|
+
const auth_wall_1 = require("../../shared/auth-wall");
|
|
32
33
|
const audit_1 = require("./audit");
|
|
33
34
|
const log_1 = require("./log");
|
|
34
35
|
const tasks_1 = require("../bridge/tasks");
|
|
@@ -40,6 +41,7 @@ const TARGET_PROPS = {
|
|
|
40
41
|
ref: zod_1.z.string().describe('Element ref from a prior read (exactly one of selector|ref)').optional(),
|
|
41
42
|
};
|
|
42
43
|
const tabIdField = zod_1.z.string().describe('Target tab id (defaults to the active tab)').optional();
|
|
44
|
+
const authWallField = zod_1.z.boolean().describe('Fail with [AUTH_REQUIRED] when the page this call lands on is a high-confidence sign-in wall (session expired). Off by default unless the server runs with --fail-on-auth-wall; snapshot still reports the verdict as `authWall` either way.').optional();
|
|
43
45
|
/**
|
|
44
46
|
* Frame targeting. Omitted = the top frame, which is what every call did before
|
|
45
47
|
* frames were addressable. `allFrames` is the one to reach for when a selector
|
|
@@ -79,27 +81,27 @@ exports.TOOL_DEFINITIONS = [
|
|
|
79
81
|
{ name: 'tab_select', description: 'Make a tab active by tabId.', inputSchema: { tabId: zod_1.z.string() } },
|
|
80
82
|
{ name: 'tab_new', description: 'Open a NEW tab (optionally at a URL) and focus it. Prefer this over `navigate` when the user says "open"/"go to" a site — `navigate` REPLACES the current tab. Pass active:false to open in the background (used by parallel batches).', inputSchema: { url: zod_1.z.string().optional(), active: zod_1.z.boolean().optional() } },
|
|
81
83
|
{ name: 'tab_close', description: 'Close a tab by tabId.', inputSchema: { tabId: zod_1.z.string() } },
|
|
82
|
-
{ name: 'navigate', description: 'Navigate a tab to a URL, REPLACING its current page. Acts on the active tab unless tabId is given — to open a site without losing the current page, use `tab_new` instead.', inputSchema: { url: zod_1.z.string(), tabId: tabIdField, waitUntil: waitUntilField } },
|
|
83
|
-
{ name: 'back', description: 'Go back in history.', inputSchema: { tabId: tabIdField } },
|
|
84
|
-
{ name: 'forward', description: 'Go forward in history.', inputSchema: { tabId: tabIdField } },
|
|
85
|
-
{ name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: { tabId: tabIdField, waitUntil: waitUntilField } },
|
|
86
|
-
{ name: 'click', description: 'Click an element. Target by selector, a snapshot ref, or role+name (e.g. role:"button", name:"Sign in") - the locator needs no snapshot first. trusted=true uses real OS-level input.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField } },
|
|
87
|
-
{ name: 'type', description: 'Type text into an element (target by selector, ref, or role+name). trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, text: zod_1.z.string(), tabId: tabIdField, clear: zod_1.z.boolean().optional(), pressEnter: zod_1.z.boolean().optional(), keyEvents: zod_1.z.boolean().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField } },
|
|
88
|
-
{ name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField, snapshotAfter: snapshotAfterField } },
|
|
89
|
-
{ name: 'press', description: 'Press a key (with optional modifiers).', inputSchema: { key: zod_1.z.string(), modifiers: zod_1.z.array(zod_1.z.string()).optional(), tabId: tabIdField } },
|
|
84
|
+
{ name: 'navigate', description: 'Navigate a tab to a URL, REPLACING its current page. Acts on the active tab unless tabId is given — to open a site without losing the current page, use `tab_new` instead.', inputSchema: { url: zod_1.z.string(), tabId: tabIdField, waitUntil: waitUntilField, failOnAuthWall: authWallField } },
|
|
85
|
+
{ name: 'back', description: 'Go back in history.', inputSchema: { tabId: tabIdField, failOnAuthWall: authWallField } },
|
|
86
|
+
{ name: 'forward', description: 'Go forward in history.', inputSchema: { tabId: tabIdField, failOnAuthWall: authWallField } },
|
|
87
|
+
{ name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: { tabId: tabIdField, waitUntil: waitUntilField, failOnAuthWall: authWallField } },
|
|
88
|
+
{ name: 'click', description: 'Click an element. Target by selector, a snapshot ref, or role+name (e.g. role:"button", name:"Sign in") - the locator needs no snapshot first. trusted=true uses real OS-level input.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
|
|
89
|
+
{ name: 'type', description: 'Type text into an element (target by selector, ref, or role+name). trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, text: zod_1.z.string(), tabId: tabIdField, clear: zod_1.z.boolean().optional(), pressEnter: zod_1.z.boolean().optional(), keyEvents: zod_1.z.boolean().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
|
|
90
|
+
{ name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField, snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
|
|
91
|
+
{ name: 'press', description: 'Press a key (with optional modifiers).', inputSchema: { key: zod_1.z.string(), modifiers: zod_1.z.array(zod_1.z.string()).optional(), tabId: tabIdField, failOnAuthWall: authWallField } },
|
|
90
92
|
{ name: 'hover', description: 'Hover over an element.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, snapshotAfter: snapshotAfterField } },
|
|
91
93
|
{ name: 'scroll', description: 'Scroll the page or to an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, x: zod_1.z.number().optional(), y: zod_1.z.number().optional(), deltaX: zod_1.z.number().optional(), deltaY: zod_1.z.number().optional(), tabId: tabIdField } },
|
|
92
|
-
{ name: 'screenshot', description: 'Capture a
|
|
94
|
+
{ name: 'screenshot', description: 'Capture a screenshot (page or element). Default is JPEG (quality 70) at CSS-pixel size, which is several times smaller than PNG and reads fine. Pass format:"png" for lossless, quality 1-100 for JPEG, scale 2 for device pixels on a Retina display or 0.5 to shrink.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, fullPage: zod_1.z.boolean().optional(), format: zod_1.z.enum(['jpeg', 'png']).optional(), quality: zod_1.z.number().optional(), scale: zod_1.z.number().optional(), tabId: tabIdField } },
|
|
93
95
|
{ name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, tabId: tabIdField, maxBytes: maxBytesField } },
|
|
94
96
|
{ name: 'get_html', description: 'Get HTML of the page or an element. Output is capped (see maxBytes) and cut at a tag boundary; narrow it with `selector` rather than raising the cap when you can. Password field values are always blanked.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, outer: zod_1.z.boolean().optional(), tabId: tabIdField, maxBytes: maxBytesField } },
|
|
95
|
-
{ name: 'snapshot', description: 'Accessibility snapshot: interactive elements with refs to target by `ref` (more reliable than guessing CSS selectors). Pass diff:true to get only what changed since the last snapshot of this tab - far cheaper in a click/read loop. Password fields appear as secret:true with no value.', inputSchema: { interactiveOnly: zod_1.z.boolean().optional(), max: zod_1.z.number().optional(), diff: zod_1.z.boolean().describe('Return added/removed/changed elements since the previous snapshot of this tab instead of the whole tree').optional(), ...FRAME_PROPS, tabId: tabIdField } },
|
|
97
|
+
{ name: 'snapshot', description: 'Accessibility snapshot: interactive elements with refs to target by `ref` (more reliable than guessing CSS selectors). Pass diff:true to get only what changed since the last snapshot of this tab - far cheaper in a click/read loop. Password fields appear as secret:true with no value.', inputSchema: { interactiveOnly: zod_1.z.boolean().optional(), max: zod_1.z.number().optional(), diff: zod_1.z.boolean().describe('Return added/removed/changed elements since the previous snapshot of this tab instead of the whole tree').optional(), failOnAuthWall: authWallField, ...FRAME_PROPS, tabId: tabIdField } },
|
|
96
98
|
{ name: 'get_cookies', description: "Read cookies visible to the tab's URL (or a given url).", inputSchema: { url: zod_1.z.string().optional(), tabId: tabIdField } },
|
|
97
99
|
{ name: 'storage', description: 'Read/write localStorage (or sessionStorage). op: get|set|remove|clear.', inputSchema: { op: zod_1.z.enum(['get', 'set', 'remove', 'clear']), key: zod_1.z.string().optional(), value: zod_1.z.string().optional(), session: zod_1.z.boolean().optional(), tabId: tabIdField } },
|
|
98
100
|
{ name: 'eval', description: 'Evaluate JavaScript in the page (disabled in safe-mode).', inputSchema: { expression: zod_1.z.string(), awaitPromise: zod_1.z.boolean().optional(), ...FRAME_PROPS, tabId: tabIdField } },
|
|
99
|
-
{ name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema: { selector: zod_1.z.string().optional(), textContains: zod_1.z.string().optional(), gone: zod_1.z.boolean().optional(), timeoutMs: zod_1.z.number().optional(), ...FRAME_PROPS, tabId: tabIdField } },
|
|
101
|
+
{ name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema: { selector: zod_1.z.string().optional(), textContains: zod_1.z.string().optional(), gone: zod_1.z.boolean().optional(), timeoutMs: zod_1.z.number().optional(), ...FRAME_PROPS, tabId: tabIdField, failOnAuthWall: authWallField } },
|
|
100
102
|
{ name: 'extract_links', description: 'Extract anchors from the page or a subtree. dedupe=true collapses links sharing an href (nav/footer noise); limit caps the count.', inputSchema: { selector: zod_1.z.string().optional(), sameOriginOnly: zod_1.z.boolean().optional(), dedupe: zod_1.z.boolean().optional(), limit: zod_1.z.number().optional(), ...FRAME_PROPS, tabId: tabIdField } },
|
|
101
103
|
{ name: 'read_as_markdown', description: 'Read the page (or subtree) as readable markdown.', inputSchema: { selector: zod_1.z.string().optional(), ...FRAME_PROPS, tabId: tabIdField, maxBytes: maxBytesField } },
|
|
102
|
-
{ name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema: { fields: zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.boolean()])), submitSelector: zod_1.z.string().optional(), ...FRAME_PROPS, tabId: tabIdField } },
|
|
104
|
+
{ name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema: { fields: zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.boolean()])), submitSelector: zod_1.z.string().optional(), ...FRAME_PROPS, tabId: tabIdField, failOnAuthWall: authWallField } },
|
|
103
105
|
{ name: 'download_file', description: 'Download a file by URL or from a link element.', inputSchema: { url: zod_1.z.string().optional(), ...TARGET_PROPS, suggestedName: zod_1.z.string().optional(), tabId: tabIdField } },
|
|
104
106
|
{ name: 'upload_file', description: 'Set local file(s) on a file <input> (target by selector or ref) — uploads without the OS dialog. Requires --enable-uploads. `files` are absolute local paths.', inputSchema: { ...TARGET_PROPS, files: zod_1.z.array(zod_1.z.string()), tabId: tabIdField } },
|
|
105
107
|
{
|
|
@@ -161,6 +163,7 @@ exports.TOOL_DEFINITIONS = [
|
|
|
161
163
|
},
|
|
162
164
|
},
|
|
163
165
|
{ name: 'chrome_status', description: 'Report backend/session status.', inputSchema: {} },
|
|
166
|
+
{ name: 'auth_check', description: 'Is the tab sitting on a sign-in wall? Reads the page (URL, title, password fields, sign-in controls) and returns { authRequired, confidence, signals }. Use it after a navigate, or whenever a step fails unexpectedly, to tell "the session expired" apart from "the agent got lost". Pass failOnAuthWall:true to get an [AUTH_REQUIRED] error instead of a verdict, so a harness can bucket the run as an auth failure.', inputSchema: { failOnAuthWall: authWallField, ...FRAME_PROPS, tabId: tabIdField } },
|
|
164
167
|
{ name: 'profile_use', description: 'Switch the active browser profile (identity). Subsequent downloads, results, screenshots, and the action log are stored under profiles/<name>/. Resets the active task to "default" unless you then call task_new.', inputSchema: { name: zod_1.z.string().describe('Profile name (becomes a folder; sanitized to a safe path segment).') } },
|
|
165
168
|
{ name: 'task_new', description: 'Start a new task (run) under the active profile. Creates profiles/<profile>/tasks/<name>/ with downloads/, results/, screenshots/ and makes it the active task so all captured artifacts land there.', inputSchema: { name: zod_1.z.string().describe('Task name (becomes a folder; sanitized to a safe path segment).') } },
|
|
166
169
|
{ name: 'tasks_list', description: 'List every task across all profiles under the data dir, with sizes and download counts.', inputSchema: {} },
|
|
@@ -198,15 +201,15 @@ const GATE_CONTEXT = 'cannot resolve the target tab URL for the policy gate';
|
|
|
198
201
|
*
|
|
199
202
|
* Prefers a URL the backend already reported over asking again: the extension
|
|
200
203
|
* rides the tab's landing URL home on every result frame, which is what keeps a
|
|
201
|
-
* gated call to ONE round-trip instead of two.
|
|
202
|
-
* the
|
|
204
|
+
* gated call to ONE round-trip instead of two. The active-tab cache serves calls
|
|
205
|
+
* without a `tabId`; the per-tab cache (fed by results for that tab and by any
|
|
206
|
+
* `tabs_list`) serves explicitly-targeted ones, so a parallel batch over N tabs
|
|
207
|
+
* gates on the one listing that opened it rather than N more.
|
|
203
208
|
*/
|
|
204
209
|
async function gatedUrl(ex, tabId) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return known;
|
|
209
|
-
}
|
|
210
|
+
const known = tabId ? ex.cachedTabUrl?.(tabId) : ex.cachedActiveUrl?.();
|
|
211
|
+
if (known)
|
|
212
|
+
return known;
|
|
210
213
|
let tabs;
|
|
211
214
|
try {
|
|
212
215
|
tabs = await ex.tabsList();
|
|
@@ -317,6 +320,51 @@ function redaction(policy) {
|
|
|
317
320
|
return cfg;
|
|
318
321
|
}
|
|
319
322
|
/** The scope a tab's remembered snapshot lives under. */
|
|
323
|
+
/**
|
|
324
|
+
* Throw `[AUTH_REQUIRED]` when the caller asked for it and the page is a
|
|
325
|
+
* high-confidence sign-in wall. Medium-confidence verdicts never fail a call: a
|
|
326
|
+
* lone password field on an otherwise ordinary page is not worth aborting over.
|
|
327
|
+
*/
|
|
328
|
+
function failIfAuthWall(wall, url, a, policy) {
|
|
329
|
+
if (!wall || wall.confidence !== 'high')
|
|
330
|
+
return;
|
|
331
|
+
if (!authGuardOn(a, policy))
|
|
332
|
+
return;
|
|
333
|
+
throw new types_1.ExecutorError('AUTH_REQUIRED', (0, auth_wall_1.describeAuthWall)(wall, url));
|
|
334
|
+
}
|
|
335
|
+
/** The guard is on for this call when the caller asked, or the server runs with `--fail-on-auth-wall`. */
|
|
336
|
+
function authGuardOn(a, policy) {
|
|
337
|
+
return (0, validators_1.optionalBoolean)(a, 'failOnAuthWall') === true || policy.failOnAuthWall === true;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* After an action or history move: when the guard is on, look at the page the
|
|
341
|
+
* tab landed on and fail with `AUTH_REQUIRED` if it is a sign-in wall. Costs
|
|
342
|
+
* one snapshot round-trip, and only when the guard is on. The snapshot is
|
|
343
|
+
* remembered for this tab so a later `snapshot { diff: true }` stays coherent.
|
|
344
|
+
*/
|
|
345
|
+
async function guardAuthWall(ctx, a) {
|
|
346
|
+
if (!authGuardOn(a, ctx.policy))
|
|
347
|
+
return;
|
|
348
|
+
const snap = await ctx.ex.snapshot({ tabId: tabId(a), interactiveOnly: true, max: 200 });
|
|
349
|
+
(0, snapdiff_1.rememberSnapshot)(snapScope(a), snap);
|
|
350
|
+
failIfAuthWall((0, auth_wall_1.detectAuthWall)(snap), snap.url, a, ctx.policy);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* A wait that timed out on a page that has become a sign-in wall is an auth
|
|
354
|
+
* failure, not a slow page. With the guard on, reclassify it.
|
|
355
|
+
*/
|
|
356
|
+
async function reclassifyTimeout(ctx, a, err) {
|
|
357
|
+
if (err instanceof types_1.ExecutorError && err.code === 'TIMEOUT' && authGuardOn(a, ctx.policy)) {
|
|
358
|
+
await guardAuthWall(ctx, a);
|
|
359
|
+
}
|
|
360
|
+
throw err;
|
|
361
|
+
}
|
|
362
|
+
/** The `authWall` field to spread onto a page-read result: present only when detected. */
|
|
363
|
+
function authWallOf(ctx, snap, a) {
|
|
364
|
+
const wall = (0, auth_wall_1.detectAuthWall)(snap);
|
|
365
|
+
failIfAuthWall(wall, snap.url, a, ctx.policy);
|
|
366
|
+
return wall ? { authWall: wall } : {};
|
|
367
|
+
}
|
|
320
368
|
const snapScope = (a) => (0, snapdiff_1.scopeOf)((0, workspace_1.peekActiveWorkspace)()?.profile ?? 'default', tabId(a));
|
|
321
369
|
/**
|
|
322
370
|
* Render an action's result, optionally with what the action CHANGED on the
|
|
@@ -324,13 +372,20 @@ const snapScope = (a) => (0, snapdiff_1.scopeOf)((0, workspace_1.peekActiveWorks
|
|
|
324
372
|
* re-read, which is the expensive half of every click-then-look loop.
|
|
325
373
|
*/
|
|
326
374
|
async function actionResult(ctx, a, payload) {
|
|
327
|
-
|
|
375
|
+
const wantDiff = (0, validators_1.optionalBoolean)(a, 'snapshotAfter') === true;
|
|
376
|
+
const guard = authGuardOn(a, ctx.policy);
|
|
377
|
+
if (!wantDiff && !guard)
|
|
328
378
|
return (0, envelopes_1.jsonResult)(payload);
|
|
329
379
|
const scope = snapScope(a);
|
|
330
380
|
const previous = (0, snapdiff_1.lastSnapshot)(scope);
|
|
331
381
|
const snap = await ctx.ex.snapshot({ tabId: tabId(a), max: 200, ...frameOpts(a) });
|
|
332
|
-
|
|
382
|
+
// One snapshot serves both: the auth guard reads it first (and aborts the
|
|
383
|
+
// call if the action landed on a sign-in wall), then the diff is built.
|
|
384
|
+
failIfAuthWall((0, auth_wall_1.detectAuthWall)(snap), snap.url, a, ctx.policy);
|
|
333
385
|
const stored = (0, snapdiff_1.rememberSnapshot)(scope, snap);
|
|
386
|
+
if (!wantDiff)
|
|
387
|
+
return (0, envelopes_1.jsonResult)(payload);
|
|
388
|
+
const diff = (0, snapdiff_1.diffSnapshots)(previous, snap);
|
|
334
389
|
return (0, envelopes_1.jsonResult)({ ...payload, changed: { ...diff, snapshotId: stored.id, url: snap.url } });
|
|
335
390
|
}
|
|
336
391
|
/**
|
|
@@ -403,19 +458,31 @@ exports.TOOL_HANDLERS = {
|
|
|
403
458
|
navigate: async (a, ctx) => {
|
|
404
459
|
const url = (0, validators_1.requireString)(a, 'url');
|
|
405
460
|
await gate(ctx, 'navigate', { url });
|
|
406
|
-
|
|
461
|
+
const nav = await ctx.ex.navigate({ url, tabId: tabId(a), waitUntil: waitUntil(a) });
|
|
462
|
+
if (!authGuardOn(a, ctx.policy))
|
|
463
|
+
return (0, envelopes_1.jsonResult)(nav);
|
|
464
|
+
// Guard on: the check costs one snapshot round-trip after the navigation.
|
|
465
|
+
const snap = await ctx.ex.snapshot({ tabId: tabId(a), interactiveOnly: true, max: 200 });
|
|
466
|
+
(0, snapdiff_1.rememberSnapshot)(snapScope(a), snap);
|
|
467
|
+
return (0, envelopes_1.jsonResult)({ ...nav, ...authWallOf(ctx, snap, a) });
|
|
407
468
|
},
|
|
408
469
|
back: async (a, ctx) => {
|
|
409
470
|
await gate(ctx, 'back', { tabId: tabId(a) });
|
|
410
|
-
|
|
471
|
+
const res = await ctx.ex.back(tabId(a));
|
|
472
|
+
await guardAuthWall(ctx, a);
|
|
473
|
+
return (0, envelopes_1.jsonResult)(res);
|
|
411
474
|
},
|
|
412
475
|
forward: async (a, ctx) => {
|
|
413
476
|
await gate(ctx, 'forward', { tabId: tabId(a) });
|
|
414
|
-
|
|
477
|
+
const res = await ctx.ex.forward(tabId(a));
|
|
478
|
+
await guardAuthWall(ctx, a);
|
|
479
|
+
return (0, envelopes_1.jsonResult)(res);
|
|
415
480
|
},
|
|
416
481
|
reload: async (a, ctx) => {
|
|
417
482
|
await gate(ctx, 'reload', { tabId: tabId(a) });
|
|
418
|
-
|
|
483
|
+
const res = await ctx.ex.reload({ tabId: tabId(a), waitUntil: waitUntil(a) });
|
|
484
|
+
await guardAuthWall(ctx, a);
|
|
485
|
+
return (0, envelopes_1.jsonResult)(res);
|
|
419
486
|
},
|
|
420
487
|
click: async (a, ctx) => {
|
|
421
488
|
await gate(ctx, 'click', { tabId: tabId(a) });
|
|
@@ -453,10 +520,12 @@ exports.TOOL_HANDLERS = {
|
|
|
453
520
|
},
|
|
454
521
|
press: async (a, ctx) => {
|
|
455
522
|
await gate(ctx, 'press', { tabId: tabId(a) });
|
|
456
|
-
|
|
523
|
+
const res = await ctx.ex.press((0, validators_1.requireString)(a, 'key'), {
|
|
457
524
|
tabId: tabId(a),
|
|
458
525
|
modifiers: (0, validators_1.optionalStringArray)(a, 'modifiers'),
|
|
459
|
-
})
|
|
526
|
+
});
|
|
527
|
+
await guardAuthWall(ctx, a); // Enter on a form is the classic way to land on a wall
|
|
528
|
+
return (0, envelopes_1.jsonResult)(res);
|
|
460
529
|
},
|
|
461
530
|
hover: async (a, ctx) => {
|
|
462
531
|
await gate(ctx, 'hover', { tabId: tabId(a) });
|
|
@@ -478,13 +547,20 @@ exports.TOOL_HANDLERS = {
|
|
|
478
547
|
},
|
|
479
548
|
screenshot: async (a, ctx) => {
|
|
480
549
|
await gate(ctx, 'screenshot', { tabId: tabId(a) });
|
|
550
|
+
const format = (0, validators_1.optionalString)(a, 'format');
|
|
551
|
+
if (format !== undefined && format !== 'jpeg' && format !== 'png') {
|
|
552
|
+
throw new validators_1.McpToolError('"format" must be "jpeg" or "png"');
|
|
553
|
+
}
|
|
481
554
|
const shot = await ctx.ex.screenshot({
|
|
482
555
|
tabId: tabId(a),
|
|
483
556
|
...frameOpts(a),
|
|
484
557
|
fullPage: (0, validators_1.optionalBoolean)(a, 'fullPage'),
|
|
485
558
|
target: (0, validators_1.optionalTarget)(a),
|
|
559
|
+
format,
|
|
560
|
+
quality: (0, validators_1.optionalNumber)(a, 'quality', { min: 1, max: 100 }),
|
|
561
|
+
scale: (0, validators_1.optionalNumber)(a, 'scale', { min: 0.1, max: 4 }),
|
|
486
562
|
});
|
|
487
|
-
(0, workspace_1.saveScreenshot)(shot.dataBase64);
|
|
563
|
+
(0, workspace_1.saveScreenshot)(shot.dataBase64, shot.mimeType === 'image/jpeg' ? 'jpg' : 'png');
|
|
488
564
|
const caption = shot.truncated ? `(truncated; full height ${shot.fullHeight}px)` : undefined;
|
|
489
565
|
return (0, envelopes_1.imageResult)(shot.dataBase64, shot.mimeType, caption);
|
|
490
566
|
},
|
|
@@ -536,14 +612,16 @@ exports.TOOL_HANDLERS = {
|
|
|
536
612
|
const scope = snapScope(a);
|
|
537
613
|
const previous = (0, snapdiff_1.lastSnapshot)(scope);
|
|
538
614
|
const stored = (0, snapdiff_1.rememberSnapshot)(scope, snap);
|
|
615
|
+
const authWall = authWallOf(ctx, snap, a);
|
|
539
616
|
if ((0, validators_1.optionalBoolean)(a, 'diff') !== true) {
|
|
540
|
-
return (0, envelopes_1.jsonResult)({ ...snap, snapshotId: stored.id });
|
|
617
|
+
return (0, envelopes_1.jsonResult)({ ...snap, snapshotId: stored.id, ...authWall });
|
|
541
618
|
}
|
|
542
619
|
const diff = (0, snapdiff_1.diffSnapshots)(previous, snap);
|
|
543
620
|
return (0, envelopes_1.jsonResult)({
|
|
544
621
|
url: snap.url,
|
|
545
622
|
title: snap.title,
|
|
546
623
|
snapshotId: stored.id,
|
|
624
|
+
...authWall,
|
|
547
625
|
...diff,
|
|
548
626
|
...(diff.since === null
|
|
549
627
|
? { note: 'no previous snapshot for this tab, so everything is reported as added' }
|
|
@@ -593,14 +671,19 @@ exports.TOOL_HANDLERS = {
|
|
|
593
671
|
},
|
|
594
672
|
wait_for: async (a, ctx) => {
|
|
595
673
|
await gate(ctx, 'wait_for', { tabId: tabId(a) });
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
674
|
+
try {
|
|
675
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.waitFor({
|
|
676
|
+
tabId: tabId(a),
|
|
677
|
+
...frameOpts(a),
|
|
678
|
+
selector: (0, validators_1.optionalString)(a, 'selector'),
|
|
679
|
+
textContains: (0, validators_1.optionalString)(a, 'textContains'),
|
|
680
|
+
gone: (0, validators_1.optionalBoolean)(a, 'gone'),
|
|
681
|
+
timeoutMs: (0, validators_1.optionalNumber)(a, 'timeoutMs', { min: 0, max: 120_000 }),
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
return reclassifyTimeout(ctx, a, err);
|
|
686
|
+
}
|
|
604
687
|
},
|
|
605
688
|
extract_links: async (a, ctx) => {
|
|
606
689
|
await gate(ctx, 'get_text', { tabId: tabId(a) }); // read of page content
|
|
@@ -643,12 +726,14 @@ exports.TOOL_HANDLERS = {
|
|
|
643
726
|
if (typeof val === 'string')
|
|
644
727
|
(0, validators_1.requireWithinLength)(val, `fields["${sel}"]`, validators_1.MAX_TEXT_LEN);
|
|
645
728
|
}
|
|
646
|
-
|
|
729
|
+
const res = await (0, helpers_1.fillForm)(ctx.ex, {
|
|
647
730
|
...frameOpts(a),
|
|
648
731
|
fields: fields,
|
|
649
732
|
submitSelector: (0, validators_1.optionalString)(a, 'submitSelector'),
|
|
650
733
|
tabId: tabId(a),
|
|
651
|
-
})
|
|
734
|
+
});
|
|
735
|
+
await guardAuthWall(ctx, a);
|
|
736
|
+
return (0, envelopes_1.jsonResult)(res);
|
|
652
737
|
},
|
|
653
738
|
download_file: async (a, ctx) => {
|
|
654
739
|
await gate(ctx, 'download_file');
|
|
@@ -768,6 +853,18 @@ exports.TOOL_HANDLERS = {
|
|
|
768
853
|
});
|
|
769
854
|
},
|
|
770
855
|
chrome_status: async (_a, ctx) => (0, envelopes_1.jsonResult)(ctx.ex.status()),
|
|
856
|
+
auth_check: async (a, ctx) => {
|
|
857
|
+
await gate(ctx, 'get_text', { tabId: tabId(a) }); // read of page structure
|
|
858
|
+
const snap = await ctx.ex.snapshot({ tabId: tabId(a), ...frameOpts(a), interactiveOnly: true, max: 200 });
|
|
859
|
+
const wall = (0, auth_wall_1.detectAuthWall)(snap);
|
|
860
|
+
failIfAuthWall(wall, snap.url, a, ctx.policy);
|
|
861
|
+
return (0, envelopes_1.jsonResult)({
|
|
862
|
+
url: snap.url,
|
|
863
|
+
title: snap.title,
|
|
864
|
+
authRequired: wall !== null,
|
|
865
|
+
...(wall ? { confidence: wall.confidence, signals: wall.signals } : {}),
|
|
866
|
+
});
|
|
867
|
+
},
|
|
771
868
|
// --- task workspace management (server-side; no browser needed) ---
|
|
772
869
|
profile_use: async (a) => {
|
|
773
870
|
// Snapshots are keyed by profile+tab, and a profile switch routes to a
|
|
@@ -877,7 +974,7 @@ function recordHistory(tool, rawArgs, ok, extra = {}) {
|
|
|
877
974
|
*/
|
|
878
975
|
const RETRY_SAFE_TOOLS = new Set([
|
|
879
976
|
'tabs_list', 'chrome_status',
|
|
880
|
-
'get_text', 'get_html', 'snapshot', 'get_cookies',
|
|
977
|
+
'get_text', 'get_html', 'snapshot', 'get_cookies', 'auth_check',
|
|
881
978
|
'extract_links', 'read_as_markdown', 'screenshot',
|
|
882
979
|
'wait_for', 'navigate', 'reload',
|
|
883
980
|
'frames_list', 'print_pdf',
|
|
@@ -32,6 +32,10 @@ export interface Policy extends WirePolicy {
|
|
|
32
32
|
redact?: boolean;
|
|
33
33
|
/** Extra redaction patterns (regex sources) supplied by the operator. */
|
|
34
34
|
redactPatterns?: string[];
|
|
35
|
+
/** `--fail-on-auth-wall`: every navigating tool and action fails with
|
|
36
|
+
* `AUTH_REQUIRED` when the page it lands on is a high-confidence sign-in
|
|
37
|
+
* wall, so an expired session is never scored as some other failure. */
|
|
38
|
+
failOnAuthWall?: boolean;
|
|
35
39
|
}
|
|
36
40
|
/** The SAFE default: deny everything until the user opts in. */
|
|
37
41
|
export declare const DEFAULT_POLICY: Readonly<Policy>;
|
|
@@ -43,6 +43,7 @@ exports.DEFAULT_POLICY = Object.freeze({
|
|
|
43
43
|
allowObservers: false,
|
|
44
44
|
redact: false,
|
|
45
45
|
redactPatterns: [],
|
|
46
|
+
failOnAuthWall: false,
|
|
46
47
|
});
|
|
47
48
|
/** Merge a partial (from a policy file and/or CLI flags) over the safe default. */
|
|
48
49
|
function resolvePolicy(partial) {
|
|
@@ -57,6 +58,7 @@ function resolvePolicy(partial) {
|
|
|
57
58
|
allowObservers: partial?.allowObservers ?? exports.DEFAULT_POLICY.allowObservers,
|
|
58
59
|
redact: partial?.redact ?? exports.DEFAULT_POLICY.redact,
|
|
59
60
|
redactPatterns: partial?.redactPatterns ?? [...(exports.DEFAULT_POLICY.redactPatterns ?? [])],
|
|
61
|
+
failOnAuthWall: partial?.failOnAuthWall ?? exports.DEFAULT_POLICY.failOnAuthWall,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
// ---------------------------------------------------------------------------
|
package/docs/BLUEPRINT.md
CHANGED
|
@@ -173,7 +173,7 @@ export interface EvalResult { ok: boolean; value?: unknown; type?: string; error
|
|
|
173
173
|
export interface WaitResult { matched: boolean; ref?: string; waitedMs: number; }
|
|
174
174
|
export interface ActionOk { ok: true; }
|
|
175
175
|
export interface ScreenshotResult {
|
|
176
|
-
dataBase64: string; mimeType: 'image/png';
|
|
176
|
+
dataBase64: string; mimeType: 'image/png' | 'image/jpeg'; // jpeg q70 @ CSS px by default; format/quality/scale opt-in
|
|
177
177
|
width: number; height: number; truncated: boolean; fullHeight?: number; // fullPage cap metadata
|
|
178
178
|
}
|
|
179
179
|
export interface DownloadResult { path: string; backend: BackendKind; bytes: number; mimeType?: string; suggestedName?: string; }
|
|
@@ -350,7 +350,7 @@ export type ExecutorErrorCode =
|
|
|
350
350
|
|
|
351
351
|
## 5. The Complete MCP Tool Surface
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
39 tools. `readOnly` is metadata (not a JSON-Schema field) consumed by the host
|
|
354
354
|
and by **safe-mode** (shipped in v1, default ON). Every handler:
|
|
355
355
|
`withReadyExecutor()` → validate args (`requireTarget` for selector|ref) →
|
|
356
356
|
**`policy.assertUrlAllowed(currentTabUrl, method)`** → call executor/helper →
|