@mehmoodqureshi/chrome-mcp 0.9.2 → 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.
@@ -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, server-side, and the caller never sees the tree. Matching runs
14
- * strongest-first (exact, then case-insensitive, then contains) so an
15
- * unambiguous name wins outright, and an ambiguous one fails loudly with the
16
- * candidates rather than silently clicking the first row.
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
- const sample = snap.nodes
83
- .filter((n) => !want.role || norm(n.role) === norm(want.role))
84
- .slice(0, 8)
85
- .map(describe);
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(', ')}. `
@@ -91,7 +91,7 @@ exports.TOOL_DEFINITIONS = [
91
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 } },
92
92
  { name: 'hover', description: 'Hover over an element.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, snapshotAfter: snapshotAfterField } },
93
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 } },
94
- { name: 'screenshot', description: 'Capture a PNG screenshot (page or element).', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, fullPage: zod_1.z.boolean().optional(), tabId: tabIdField } },
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 } },
95
95
  { name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, tabId: tabIdField, maxBytes: maxBytesField } },
96
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 } },
97
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 } },
@@ -201,15 +201,15 @@ const GATE_CONTEXT = 'cannot resolve the target tab URL for the policy gate';
201
201
  *
202
202
  * Prefers a URL the backend already reported over asking again: the extension
203
203
  * rides the tab's landing URL home on every result frame, which is what keeps a
204
- * gated call to ONE round-trip instead of two. That cache only ever describes
205
- * the active tab, so it is bypassed whenever an explicit `tabId` is in play.
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.
206
208
  */
207
209
  async function gatedUrl(ex, tabId) {
208
- if (!tabId) {
209
- const known = ex.cachedActiveUrl?.();
210
- if (known)
211
- return known;
212
- }
210
+ const known = tabId ? ex.cachedTabUrl?.(tabId) : ex.cachedActiveUrl?.();
211
+ if (known)
212
+ return known;
213
213
  let tabs;
214
214
  try {
215
215
  tabs = await ex.tabsList();
@@ -547,13 +547,20 @@ exports.TOOL_HANDLERS = {
547
547
  },
548
548
  screenshot: async (a, ctx) => {
549
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
+ }
550
554
  const shot = await ctx.ex.screenshot({
551
555
  tabId: tabId(a),
552
556
  ...frameOpts(a),
553
557
  fullPage: (0, validators_1.optionalBoolean)(a, 'fullPage'),
554
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 }),
555
562
  });
556
- (0, workspace_1.saveScreenshot)(shot.dataBase64);
563
+ (0, workspace_1.saveScreenshot)(shot.dataBase64, shot.mimeType === 'image/jpeg' ? 'jpg' : 'png');
557
564
  const caption = shot.truncated ? `(truncated; full height ${shot.fullHeight}px)` : undefined;
558
565
  return (0, envelopes_1.imageResult)(shot.dataBase64, shot.mimeType, caption);
559
566
  },
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; }