mcp-browser-extension 0.9.26 → 0.9.27

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.
@@ -310,6 +310,7 @@ export interface Executor {
310
310
  outer?: boolean;
311
311
  } & FrameOpts): Promise<{
312
312
  html: string;
313
+ frameId?: number;
313
314
  }>;
314
315
  /** Accessibility snapshot: interactive/landmark elements with stable refs the model can target. */
315
316
  snapshot(opts?: {
@@ -5,7 +5,7 @@
5
5
  * when the backend can (else sequences fill+click).
6
6
  * (Only `download_file` is privileged and lives on the executor.)
7
7
  */
8
- import type { Executor, FrameOpts } from '../executor/types';
8
+ import { type Executor, type FrameOpts } from '../executor/types';
9
9
  export interface LinkOut {
10
10
  href: string;
11
11
  text: string;
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.extractLinks = extractLinks;
11
11
  exports.readAsMarkdown = readAsMarkdown;
12
12
  exports.fillForm = fillForm;
13
+ const htmlparser2_1 = require("htmlparser2");
14
+ const types_1 = require("../executor/types");
13
15
  const markdown_extract_1 = require("./markdown-extract");
14
16
  /**
15
17
  * Collect anchors from the page (or a subtree). Implemented as a single page
@@ -25,31 +27,99 @@ async function extractLinks(ex, args) {
25
27
  const root = ${root} ? document.querySelector(${root}) : document;
26
28
  if (!root) return [];
27
29
  const here = location.origin;
28
- return [...root.querySelectorAll('a[href]')].map(a => ({
29
- href: a.href, text: (a.textContent || '').trim().slice(0, 200),
30
- })).filter(l => l.href && (${args.sameOriginOnly ? 'l.href.startsWith(here)' : 'true'}));
30
+ return [...root.querySelectorAll('a[href]')].filter(a => {
31
+ const raw = (a.getAttribute('href') || '').trim();
32
+ if (!raw || raw.startsWith('#')) return false;
33
+ try {
34
+ const url = new URL(a.href);
35
+ return !['javascript:', 'data:', 'blob:'].includes(url.protocol) &&
36
+ (${args.sameOriginOnly ? 'url.origin === here' : 'true'});
37
+ } catch { return false; }
38
+ }).map(a => ({ href: a.href, text: (a.textContent || '').trim().slice(0, 200) }));
31
39
  })()`;
32
40
  const frames = { frameId: args.frameId, allFrames: args.allFrames };
33
41
  let links;
34
- const res = await ex.eval(expr, { tabId: args.tabId, ...frames });
35
- if (res.ok && Array.isArray(res.value)) {
36
- links = res.value;
42
+ try {
43
+ const res = await ex.eval(expr, { tabId: args.tabId, ...frames });
44
+ if (res.ok && Array.isArray(res.value))
45
+ links = res.value;
37
46
  }
38
- else {
47
+ catch (err) {
48
+ if (!(err instanceof types_1.ExecutorError) || err.code !== 'POLICY_DENIED')
49
+ throw err;
50
+ }
51
+ if (!links) {
39
52
  // Fallback: parse hrefs out of the HTML (e.g. when eval is policy-denied).
40
- const { html } = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
53
+ const result = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
41
54
  tabId: args.tabId,
42
55
  ...frames,
43
56
  });
44
- links = [];
45
- const re = /<a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
46
- let m;
47
- while ((m = re.exec(html)) !== null) {
48
- links.push({ href: m[1], text: m[2].replace(/<[^>]+>/g, '').trim().slice(0, 200) });
57
+ const tabs = await ex.tabsList();
58
+ const tab = args.tabId ? tabs.find(t => t.tabId === args.tabId) : tabs.find(t => t.active);
59
+ let pageUrl = tab?.url;
60
+ const frameId = result.frameId ?? args.frameId;
61
+ if (frameId !== undefined && frameId !== 0) {
62
+ pageUrl = (await ex.framesList?.({ tabId: args.tabId }))?.find(f => f.frameId === frameId)?.url;
49
63
  }
64
+ if (!pageUrl)
65
+ throw new types_1.ExecutorError('TARGET_GONE', 'could not resolve the page URL for link extraction');
66
+ links = linksFromHtml(result.html, pageUrl, args.sameOriginOnly);
50
67
  }
51
68
  return { links: refineLinks(links, args) };
52
69
  }
70
+ /** Parse serialized HTML without executing page code; the parser decodes entities. */
71
+ function linksFromHtml(html, pageUrl, sameOriginOnly = false) {
72
+ const raw = [];
73
+ let current;
74
+ let baseHref;
75
+ let skippedDepth = 0;
76
+ new htmlparser2_1.Parser({
77
+ onopentag(name, attrs) {
78
+ if (skippedDepth || ['script', 'style', 'template'].includes(name)) {
79
+ skippedDepth++;
80
+ return;
81
+ }
82
+ if (name === 'base' && baseHref === undefined && attrs.href !== undefined)
83
+ baseHref = attrs.href;
84
+ if (name === 'a' && attrs.href !== undefined) {
85
+ current = { href: attrs.href.trim(), text: '' };
86
+ raw.push(current);
87
+ }
88
+ },
89
+ ontext(text) { if (current && !skippedDepth)
90
+ current.text += text; },
91
+ onclosetag(name) {
92
+ if (skippedDepth) {
93
+ skippedDepth--;
94
+ return;
95
+ }
96
+ if (name === 'a')
97
+ current = undefined;
98
+ },
99
+ }, { decodeEntities: true }).end(html);
100
+ const here = new URL(pageUrl);
101
+ let base = here.href;
102
+ try {
103
+ if (baseHref !== undefined)
104
+ base = new URL(baseHref, here).href;
105
+ }
106
+ catch { /* ignore invalid base */ }
107
+ return raw.flatMap(link => {
108
+ if (!link.href || link.href.startsWith('#'))
109
+ return [];
110
+ try {
111
+ const url = new URL(link.href, base);
112
+ if (['javascript:', 'data:', 'blob:'].includes(url.protocol))
113
+ return [];
114
+ if (sameOriginOnly && url.origin !== here.origin)
115
+ return [];
116
+ return [{ href: /^[a-z][a-z\d+.-]*:/i.test(link.href) ? link.href : url.href, text: link.text.trim().slice(0, 200) }];
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ });
122
+ }
53
123
  /**
54
124
  * Collapse links that share an href (keeping the first, but preferring a
55
125
  * non-empty label) when `dedupe` is set, then cap to `limit`. Order is
@@ -857,7 +857,7 @@ exports.TOOL_HANDLERS = {
857
857
  const res = await ctx.ex.getText((0, validators_1.optionalTarget)(a), { tabId: tabId(a), ...frameOpts(a) });
858
858
  // Save the FULL read before capping — the artifact on disk stays lossless;
859
859
  // only what crosses into the caller's context is bounded.
860
- (0, workspace_1.saveResult)('get_text', 'json', JSON.stringify(res, null, 2));
860
+ const savedTo = (0, workspace_1.saveResult)('get_text', 'json', JSON.stringify(res, null, 2));
861
861
  const clean = (0, redact_1.redactText)(res.text, redaction(ctx.policy));
862
862
  (0, audit_1.noteRedactions)(clean.redactions);
863
863
  const capped = (0, limits_1.capText)(clean.value, maxBytes(a));
@@ -865,6 +865,7 @@ exports.TOOL_HANDLERS = {
865
865
  return (0, envelopes_1.jsonResult)({
866
866
  ...res,
867
867
  text: capped.text,
868
+ ...(savedTo ? { savedTo } : {}),
868
869
  ...(0, limits_1.truncationMeta)(capped),
869
870
  ...(clean.redactions ? { redactions: clean.redactions } : {}),
870
871
  });
@@ -993,7 +994,7 @@ exports.TOOL_HANDLERS = {
993
994
  tabId: tabId(a),
994
995
  ...frameOpts(a),
995
996
  });
996
- (0, workspace_1.saveResult)('read_as_markdown', 'md', md);
997
+ const savedTo = (0, workspace_1.saveResult)('read_as_markdown', 'md', md);
997
998
  const clean = (0, redact_1.redactText)(md, redaction(ctx.policy));
998
999
  (0, audit_1.noteRedactions)(clean.redactions);
999
1000
  const capped = (0, limits_1.capText)(clean.value, maxBytes(a));
@@ -1001,7 +1002,7 @@ exports.TOOL_HANDLERS = {
1001
1002
  // Markdown is returned as plain text, so the truncation notice has to ride in
1002
1003
  // the body rather than as sibling JSON fields.
1003
1004
  return (0, envelopes_1.textResult)(capped.truncated
1004
- ? `${capped.text}\n\n[truncated: ${capped.returnedBytes} of ${capped.totalBytes} bytes — raise maxBytes or narrow with selector; the full document was saved to the task's results/ dir]`
1005
+ ? `${capped.text}\n\n[truncated: ${capped.returnedBytes} of ${capped.totalBytes} bytes — raise maxBytes or narrow with selector${savedTo ? `; full document saved to ${savedTo}` : '; full document could not be saved'}]`
1005
1006
  : capped.text);
1006
1007
  },
1007
1008
  fill_form: async (a, ctx) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-browser-extension",
3
3
  "mcpName": "io.github.Mehmoodqureshi/mcp-browser-extension",
4
- "version": "0.9.26",
4
+ "version": "0.9.27",
5
5
  "description": "Drive your real Chrome browser over MCP \u2014 real logins, real cookies. A stdio MCP server (CLI) plus an MV3 extension, driving Chrome via chrome.scripting/chrome.tabs. Multi-tab batch automation, accessibility snapshots, deny-all security by default.",
6
6
  "author": "Mehmood Ur Rehman Qureshi",
7
7
  "license": "MIT",
@@ -61,7 +61,8 @@
61
61
  "prepack": "npm run clean && npm run build && npm run build:ext"
62
62
  },
63
63
  "dependencies": {
64
- "@modelcontextprotocol/sdk": "^1.29.0",
64
+ "@modelcontextprotocol/sdk": "^1.32.1",
65
+ "htmlparser2": "^10.1.0",
65
66
  "ws": "^8.18.0",
66
67
  "zod": "^4.4.3"
67
68
  },