@extension.dev/mcp 10.10.0 → 10.10.1

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,7 +10,7 @@
10
10
  "name": "extension-mcp",
11
11
  "source": "./",
12
12
  "description": "MCP tools for browser extension development: scaffold from 50+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox.",
13
- "version": "10.10.0",
13
+ "version": "10.10.1",
14
14
  "category": "development",
15
15
  "author": {
16
16
  "name": "Cezar Augusto"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "extension-mcp",
3
3
  "description": "MCP tools for browser extension development: scaffold from 50+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox. Ships /extension, /extension-add, /extension-debug, and /extension-publish commands.",
4
- "version": "10.10.0",
4
+ "version": "10.10.1",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 10.10.1
4
+
5
+ Five Gecko findings from one agent session on Firefox and Waterfox
6
+ (BUGS_TO_FIX_MCP.md entries 5 to 9), each replayed against a live Firefox
7
+ Nightly before and after the fix where the shape allowed it, plus four more
8
+ that followed them the same week, and the engine release that carries the
9
+ root-cause fix for the first of them.
10
+
11
+ - `extension-create`, `extension-develop` and `extension-install` move from
12
+ 4.1.29 to 4.1.30. That engine settles a promise before a surface relay
13
+ replies to an eval (the root cause behind the first entry below, filed
14
+ from this repo as extension.js 512), names a missing tab, a refused url,
15
+ a closed surface and a browser exit on every act verb instead of a
16
+ generic refusal, and lets `reload` resolve a surface's tab. The relay
17
+ wrapper below stays: a project that pins an older engine in its own
18
+ node_modules still drives that engine.
19
+
20
+ - `extension_eval` on a surface that answers through the in-bundle relay
21
+ (every Gecko session, and Chromium MV2) never hands the relay a promise.
22
+ The relay evaluates synchronously and passes the raw value to
23
+ `sendResponse`; a promise is not cloneable, Firefox reports that to the
24
+ sender as no reply, and the bridge then called the surface "not open"
25
+ while the page was still running the expression. The wrapper now settles
26
+ a thenable inside the page under a token, and the tool polls until it is
27
+ done: an async expression returns its value, a rejection answers `E_EVAL`
28
+ with the page's own message, and one that never settles answers
29
+ `E_WAIT_TIMEOUT` naming where the result will land. The engine's "open it
30
+ first: extension open newtab" hint is now spoken as the tool call it means.
31
+ - `extension_inspect` on Gecko reads a page inside the extension through
32
+ its surface relay: a url that matches a declared surface document (the
33
+ full `moz-extension://` address, the document path, or its file name)
34
+ routes to that context instead of the tab injection, which Firefox
35
+ refuses into extension pages with "Missing host permission for the tab".
36
+ An extension url that matches no declared surface says so and names the
37
+ ones the manifest declares.
38
+ - `extension_stop` reaps the browser the launcher recorded in `ready.json`
39
+ (`browserPid`, `launcherPid`, and any process holding `profilePath`), so
40
+ a Firefox started on a custom profile, or one whose argv never names the
41
+ project, no longer survives a `stopped` answer with `reaped: []`.
42
+ - `extension_doctor` no longer calls a Gecko session unhealthy over a
43
+ browser exit that the executor outlived: Firefox hands a fresh profile to
44
+ a relaunched process and the first one exits 0, which the engine's browser
45
+ leg reports as a failure while storage probes and evals keep answering.
46
+ That leg becomes a warning that says so, and any other failure still
47
+ counts.
48
+ - `extension_browsers` detect finds a managed binary as deep as the cache
49
+ writes it (a Firefox Nightly sits six levels down inside its app bundle),
50
+ so it no longer reports the system Firefox as available while
51
+ `extension_dev` launches the cached Nightly. When both exist, the entry
52
+ carries `systemBinaryPath` and a `devLaunches` line saying which one dev
53
+ starts and how to choose the other. `extension_wait` reports `browserPid`
54
+ and `profilePath` from the contract.
55
+ - `extension_open` with surface `sidebar` on Gecko no longer stops at the
56
+ engine's "sidePanel not available", a Chromium API named on a Firefox
57
+ engine. It asks the sidebar relay whether the `sidebar_action` panel is
58
+ open and reports `status: "already-open"` when it is (Firefox opens the
59
+ panel at install, so it usually is); when it is not, the document is
60
+ rendered as a tab with the gesture rule stated (Firefox opens the panel
61
+ only from the toolbar button or View > Sidebar, Bugzilla 1392624).
62
+ - `extension_eval` with context `page` and a `moz-extension://` url on
63
+ Gecko evaluates through the surface relay of the document that url names,
64
+ with a warning naming the context to pass next time, instead of "chrome.
65
+ scripting is not available ... use context background". An extension url
66
+ that matches no declared surface says which surfaces the manifest
67
+ declares. The url-to-surface mapping now lives in one place for inspect
68
+ and eval.
69
+ - `extension_dev` with `port: 0` no longer warns "Requested port 0 was not
70
+ available": 0 asks the engine for any free port, so the note now says
71
+ which port it picked, and the collision wording stays for a numbered port
72
+ the server could not bind.
73
+
3
74
  ## 10.10.0
4
75
 
5
76
  Safari was the one engine this server treated as a dead end, while the
package/dist/module.js CHANGED
@@ -192,6 +192,7 @@ __webpack_require__.d(start_namespaceObject, {
192
192
  var stop_namespaceObject = {};
193
193
  __webpack_require__.r(stop_namespaceObject);
194
194
  __webpack_require__.d(stop_namespaceObject, {
195
+ contractProcessHints: ()=>contractProcessHints,
195
196
  handler: ()=>stop_handler,
196
197
  schema: ()=>stop_schema,
197
198
  stopOne: ()=>stopOne
@@ -227,7 +228,7 @@ __webpack_require__.d(wait_namespaceObject, {
227
228
  handler: ()=>wait_handler,
228
229
  schema: ()=>wait_schema
229
230
  });
230
- var package_namespaceObject = JSON.parse('{"rE":"10.10.0","El":{"OP":"4.1.29"}}');
231
+ var package_namespaceObject = JSON.parse('{"rE":"10.10.1","El":{"OP":"4.1.30"}}');
231
232
  const ENVELOPE_SCHEMA = 1;
232
233
  const collectWarnings = (warnings)=>{
233
234
  if (!warnings) return [];
@@ -4104,8 +4105,31 @@ function processCommand(pid) {
4104
4105
  return "";
4105
4106
  }
4106
4107
  }
4107
- function sessionProcessPids(projectPath) {
4108
+ function contractProcessHints(projectPath, browser) {
4109
+ try {
4110
+ const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
4111
+ const pids = [
4112
+ contract.browserPid,
4113
+ contract.launcherPid
4114
+ ].filter((pid)=>"number" == typeof pid && Number.isInteger(pid) && pid > 0);
4115
+ return {
4116
+ ..."string" == typeof contract.profilePath && contract.profilePath.trim() ? {
4117
+ profilePath: contract.profilePath
4118
+ } : {},
4119
+ pids
4120
+ };
4121
+ } catch {
4122
+ return {
4123
+ pids: []
4124
+ };
4125
+ }
4126
+ }
4127
+ function sessionProcessPids(projectPath, hints = {
4128
+ pids: []
4129
+ }) {
4108
4130
  const pids = new Set();
4131
+ if (hints.profilePath) for (const pid of pgrepPids(escapeRegex(hints.profilePath)))pids.add(pid);
4132
+ for (const pid of hints.pids)if (pid !== process.pid && isAlive(pid)) pids.add(pid);
4109
4133
  for (const form of projectPathForms(projectPath)){
4110
4134
  const escaped = escapeRegex(form);
4111
4135
  const patterns = [
@@ -4118,8 +4142,10 @@ function sessionProcessPids(projectPath) {
4118
4142
  ...pids
4119
4143
  ].filter((pid)=>PLAUSIBLE_SESSION_BINARY.test(processCommand(pid)));
4120
4144
  }
4121
- function reapSessionProcesses(projectPath) {
4122
- const pids = sessionProcessPids(projectPath);
4145
+ function reapSessionProcesses(projectPath, hints = {
4146
+ pids: []
4147
+ }) {
4148
+ const pids = sessionProcessPids(projectPath, hints);
4123
4149
  for (const pid of pids)try {
4124
4150
  process.kill(pid, "SIGKILL");
4125
4151
  } catch {}
@@ -4158,8 +4184,9 @@ function pidFromReadyContract(projectPath, browser) {
4158
4184
  async function stopOne(projectPath, browser) {
4159
4185
  const session = findSessionInfo(projectPath, browser);
4160
4186
  const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
4187
+ const hints = contractProcessHints(projectPath, browser);
4161
4188
  if (null == pid) {
4162
- const reaped = reapSessionProcesses(projectPath);
4189
+ const reaped = reapSessionProcesses(projectPath, hints);
4163
4190
  removeSessionMarker(projectPath, browser);
4164
4191
  return {
4165
4192
  projectPath,
@@ -4181,7 +4208,7 @@ async function stopOne(projectPath, browser) {
4181
4208
  }
4182
4209
  detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
4183
4210
  } else detail = "Process was already gone; cleaned up session records.";
4184
- const reaped = reapSessionProcesses(projectPath);
4211
+ const reaped = reapSessionProcesses(projectPath, hints);
4185
4212
  removeSession(projectPath, browser);
4186
4213
  removeSessionMarker(projectPath, browser);
4187
4214
  try {
@@ -4189,7 +4216,7 @@ async function stopOne(projectPath, browser) {
4189
4216
  force: true
4190
4217
  });
4191
4218
  } catch {}
4192
- const survivors = sessionProcessPids(projectPath);
4219
+ const survivors = sessionProcessPids(projectPath, hints);
4193
4220
  const stopped = !isAlive(pid) && 0 === survivors.length;
4194
4221
  if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
4195
4222
  else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
@@ -4553,7 +4580,8 @@ async function dev_handler(args) {
4553
4580
  } : {
4554
4581
  requestedPort: args.port ?? 8080
4555
4582
  };
4556
- const portNote = null !== boundPort ? void 0 !== args.port && args.port !== boundPort ? `Requested port ${args.port} was not available; the dev server bound ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : null : "The engine has not stamped its ready.json contract yet, so the bound port is not known at response time (a taken port makes the server bind the next free one). extension_wait reports the bound port from that contract once it lands; requestedPort above is only what was asked for.";
4583
+ const autoAssigned = 0 === args.port;
4584
+ const portNote = null !== boundPort ? void 0 !== args.port && args.port !== boundPort ? autoAssigned ? `Port 0 asked the engine for any free port and it picked ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : `Requested port ${args.port} was not available; the dev server bound ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : null : autoAssigned ? "Port 0 asked the engine for any free port, and it has not stamped its ready.json contract yet, so the port it picked is not known at response time. extension_wait reports it from that contract once it lands." : "The engine has not stamped its ready.json contract yet, so the bound port is not known at response time (a taken port makes the server bind the next free one). extension_wait reports the bound port from that contract once it lands; requestedPort above is only what was asked for.";
4557
4585
  return envelope_envelope({
4558
4586
  ok: true,
4559
4587
  command: dev_schema.name,
@@ -4743,7 +4771,7 @@ async function start_handler(args) {
4743
4771
  });
4744
4772
  }
4745
4773
  function toMcpSpeak(text) {
4746
- return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
4774
+ return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/\bextension open (popup|options|sidebar|newtab|history|bookmarks)\b/g, 'extension_open with surface: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
4747
4775
  }
4748
4776
  const CONTROL_CHANNEL_DOWN_CODES = new Set([
4749
4777
  "E_CONTROL_DENIED",
@@ -6606,6 +6634,10 @@ async function open_handler(args) {
6606
6634
  const refusal = readGestureRefusal(raw);
6607
6635
  if (refusal) return openSidebarThroughGesture(args.projectPath, browser, refusal);
6608
6636
  }
6637
+ if ("sidebar" === args.surface && isGeckoFamily(browser)) {
6638
+ const refusal = readUnsupportedRefusal(raw);
6639
+ if (refusal) return openGeckoSidebar(args.projectPath, browser, refusal, args.timeout);
6640
+ }
6609
6641
  return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
6610
6642
  }
6611
6643
  const E_USER_GESTURE_REQUIRED = "E_USER_GESTURE_REQUIRED";
@@ -6621,6 +6653,73 @@ function readGestureRefusal(raw) {
6621
6653
  const message = String(parsed.error?.message ?? "");
6622
6654
  return code === E_USER_GESTURE_REQUIRED || /user gesture/i.test(message) ? parsed : null;
6623
6655
  }
6656
+ function readUnsupportedRefusal(raw) {
6657
+ let parsed;
6658
+ try {
6659
+ parsed = JSON.parse(raw);
6660
+ } catch {
6661
+ return null;
6662
+ }
6663
+ if (parsed?.ok !== false) return null;
6664
+ const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
6665
+ const message = String(parsed.error?.message ?? "");
6666
+ return "E_NOT_IMPLEMENTED" === code || /not available/i.test(message) ? parsed : null;
6667
+ }
6668
+ const GECKO_SIDEBAR_GESTURE = "Firefox opens a sidebar_action panel only from a user gesture (the toolbar button or View > Sidebar; Bugzilla 1392624), and the engine's open verb carries none, so it cannot open the panel";
6669
+ async function openGeckoSidebar(projectPath, browser, refusal, timeout) {
6670
+ const doc = surfaceDocument(projectPath, browser, "sidebar");
6671
+ if (!doc) return missingSurfaceError(projectPath, browser, "sidebar", "so there is no sidebar panel to open");
6672
+ const probe = await runActVerb([
6673
+ "inspect",
6674
+ projectPath,
6675
+ "--context",
6676
+ "sidebar",
6677
+ "--include",
6678
+ "summary",
6679
+ "--browser",
6680
+ browser,
6681
+ ...null != timeout ? [
6682
+ "--timeout",
6683
+ String(timeout)
6684
+ ] : []
6685
+ ], projectPath, timeout, open_schema.name);
6686
+ let open = null;
6687
+ try {
6688
+ const parsed = JSON.parse(probe);
6689
+ if (parsed?.ok === true) open = parsed;
6690
+ } catch {}
6691
+ if (open) {
6692
+ const url = "string" == typeof open.value?.url ? open.value.url : "string" == typeof open.value?.meta?.url ? open.value.meta.url : void 0;
6693
+ return envelope_envelope({
6694
+ ok: true,
6695
+ command: open_schema.name,
6696
+ status: "already-open",
6697
+ value: {
6698
+ surface: "sidebar",
6699
+ document: doc,
6700
+ alreadyOpen: true,
6701
+ ...url ? {
6702
+ url
6703
+ } : {}
6704
+ },
6705
+ hint: `The sidebar panel is open in the ${browser} window already: read it with extension_dom_snapshot context: 'sidebar' or run code in it with extension_eval context: 'sidebar'. ${GECKO_SIDEBAR_GESTURE}, and it did not need to.`
6706
+ });
6707
+ }
6708
+ const fallback = await openSurfaceAsTab(projectPath, browser, "sidebar");
6709
+ try {
6710
+ const parsedFallback = JSON.parse(fallback);
6711
+ if (parsedFallback?.ok) {
6712
+ addWarning(parsedFallback, `${GECKO_SIDEBAR_GESTURE}, and the panel is not open now, so the sidebar document was rendered as a tab instead. The DOM is the same document the panel would show; the panel hosting stays unverified. A person opens the real panel from the toolbar button or View > Sidebar.`);
6713
+ return actFrameJson(parsedFallback);
6714
+ }
6715
+ } catch {}
6716
+ refusal.error = {
6717
+ ...refusal.error ?? {},
6718
+ message: `${GECKO_SIDEBAR_GESTURE}. Rendering the document ${doc} as a tab failed as well.`
6719
+ };
6720
+ refusal.hint = "Start the session with allowEval: true so the document can be opened by url through the bridge, or open the panel from the toolbar button in the dev browser and read it with extension_dom_snapshot context: 'sidebar'.";
6721
+ return actFrameJson(refusal);
6722
+ }
6624
6723
  const SIDEBAR_GESTURE_WARNING = "Chrome opens the side panel only from a user gesture and the engine's open verb carries none, so the server opened the extension's own sidebar page in a tab, dispatched a synthetic click on it over CDP and called chrome.sidePanel.open from inside that click, then closed the tab. The panel that opened is the real one; the toolbar wiring (action.onClicked or sidePanel.setPanelBehavior) was not exercised.";
6625
6724
  async function openSidebarThroughGesture(projectPath, browser, refusal) {
6626
6725
  const doc = surfaceDocument(projectPath, browser, "sidebar");
@@ -8949,6 +9048,25 @@ function capabilityProbeChecks(parsed) {
8949
9048
  return value?.checks;
8950
9049
  }
8951
9050
  const CONTROL_OFF_BY_CHOICE = /\bwas not started with --allow-control\b/i;
9051
+ function pidIsAlive(pid) {
9052
+ try {
9053
+ process.kill(pid, 0);
9054
+ return true;
9055
+ } catch {
9056
+ return false;
9057
+ }
9058
+ }
9059
+ function reconcileRelaunchedBrowser(checks, contract) {
9060
+ const exitedLeg = checks.find((leg)=>"browser" === leg.check && "fail" === leg.status && /\bexited\b/i.test(String(leg.detail ?? "")));
9061
+ if (!exitedLeg) return false;
9062
+ const executorAnswered = checks.some((leg)=>("executor" === leg.check || "control-channel" === leg.check) && "pass" === leg.status);
9063
+ const browserAlive = "number" == typeof contract?.browserPid && pidIsAlive(contract.browserPid);
9064
+ if (!executorAnswered && !browserAlive) return false;
9065
+ exitedLeg.status = "warn";
9066
+ exitedLeg.detail = `${exitedLeg.detail ?? "browser exited"}. ${browserAlive ? `The browser pid the launcher recorded (${contract?.browserPid}) is alive` : "The executor answered a probe after that exit"}, so the session is live and the recorded exit was an earlier process: Firefox hands a fresh profile to a relaunched process and the first one exits 0.`;
9067
+ exitedLeg.remediation = "Nothing to do. If a later call finds the session unreachable, extension_stop and extension_dev again.";
9068
+ return true;
9069
+ }
8952
9070
  function controlOffByChoiceLeg(checks) {
8953
9071
  return checks.find((leg)=>"control-channel" === leg.check && "fail" === leg.status && "string" == typeof leg.detail && CONTROL_OFF_BY_CHOICE.test(leg.detail)) ?? null;
8954
9072
  }
@@ -8991,17 +9109,9 @@ async function doctor_handler(args) {
8991
9109
  }
8992
9110
  let healthy = 0 === code;
8993
9111
  const contract = readContractForDiagnosis(projectPath, browser);
8994
- if (contract?.status === "error") {
8995
- healthy = false;
8996
- const browserExited = "browser_exited" === contract.code || void 0 !== contract.browserExitCode;
8997
- const detail = browserExited ? `The ${browser} browser for this session exited unexpectedly${null != contract.browserExitCode ? ` (exit code ${contract.browserExitCode})` : ""}; the extension may have been rejected or the browser crashed. The session cannot be driven.` : contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
8998
- checks.push({
8999
- check: "runtime-errors",
9000
- status: "fail",
9001
- detail: toMcpSpeak(detail),
9002
- remediation: browserExited ? "Read extension_logs and the session log for the rejection cause, call extension_stop to clean up, then relaunch." : "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
9003
- });
9004
- } else {
9112
+ const relaunched = reconcileRelaunchedBrowser(checks, contract);
9113
+ if (relaunched) healthy = !checks.some((leg)=>"fail" === leg.status);
9114
+ if (contract?.status !== "error" || relaunched) {
9005
9115
  const errs = recentErrorLogs(projectPath, browser);
9006
9116
  if (errs.length) {
9007
9117
  healthy = false;
@@ -9012,6 +9122,16 @@ async function doctor_handler(args) {
9012
9122
  remediation: "The extension is throwing at runtime. Inspect with extension_logs. A chrome.* API called without its permission is a common cause: extension_manifest_validate catches a permission MISSING FROM permissions[], but it does not model host-permission scope (e.g. webRequest with no matching host_permissions) or gesture requirements (e.g. activeTab without a user gesture), so a valid:true there does not rule those out."
9013
9123
  });
9014
9124
  }
9125
+ } else {
9126
+ healthy = false;
9127
+ const browserExited = "browser_exited" === contract.code || void 0 !== contract.browserExitCode;
9128
+ const detail = browserExited ? `The ${browser} browser for this session exited unexpectedly${null != contract.browserExitCode ? ` (exit code ${contract.browserExitCode})` : ""}; the extension may have been rejected or the browser crashed. The session cannot be driven.` : contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
9129
+ checks.push({
9130
+ check: "runtime-errors",
9131
+ status: "fail",
9132
+ detail: toMcpSpeak(detail),
9133
+ remediation: browserExited ? "Read extension_logs and the session log for the rejection cause, call extension_stop to clean up, then relaunch." : "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
9134
+ });
9015
9135
  }
9016
9136
  const engineVersion = projectEngineVersion(projectPath);
9017
9137
  if (engineVersion) {
@@ -11967,6 +12087,32 @@ const inspect_schema_schema = {
11967
12087
  ]
11968
12088
  }
11969
12089
  };
12090
+ const EXTENSION_PAGE_CONTEXTS = [
12091
+ "popup",
12092
+ "options",
12093
+ "sidebar",
12094
+ "newtab",
12095
+ "history",
12096
+ "bookmarks"
12097
+ ];
12098
+ const EXTENSION_ORIGIN = /^(moz|chrome|safari-web)-extension:\/\//;
12099
+ function isExtensionUrl(url) {
12100
+ return "string" == typeof url && EXTENSION_ORIGIN.test(url);
12101
+ }
12102
+ function surfaceForExtensionUrl(projectPath, browser, url) {
12103
+ const bare = url.replace(EXTENSION_ORIGIN, "").replace(/^[^/]*\//, (m)=>EXTENSION_ORIGIN.test(url) ? "" : m).replace(/^\.?\//, "").replace(/[?#].*$/, "");
12104
+ if (!bare) return null;
12105
+ for (const context of EXTENSION_PAGE_CONTEXTS){
12106
+ const document = surfaceDocument(projectPath, browser, context);
12107
+ if (document) {
12108
+ if (bare === document || bare.endsWith(`/${document}`) || document.endsWith(`/${bare}`)) return {
12109
+ context,
12110
+ document
12111
+ };
12112
+ }
12113
+ }
12114
+ return null;
12115
+ }
11970
12116
  const inspect_gecko_TOOL = inspect_schema_schema.name;
11971
12117
  function buildBridgeInspectExpression(opts) {
11972
12118
  const parts = [
@@ -12084,7 +12230,22 @@ async function collectGeckoConsole(args, browser, urlFilter, result, notes) {
12084
12230
  }
12085
12231
  async function inspectViaBridge(args, browser, include, maxBytes) {
12086
12232
  const notes = [];
12087
- if (args.url) {
12233
+ const surface = args.url ? surfaceForExtensionUrl(args.projectPath, browser, args.url) : null;
12234
+ if (args.url && !surface && EXTENSION_ORIGIN.test(args.url)) {
12235
+ const declared = declaredSurfaces(args.projectPath, browser) ?? [];
12236
+ return envelope_envelope({
12237
+ ok: false,
12238
+ command: inspect_gecko_TOOL,
12239
+ status: "no-surface",
12240
+ error: {
12241
+ code: "E_NO_SURFACE_DOCUMENT",
12242
+ name: "NoSurfaceDocument",
12243
+ message: `${args.url} is a page inside the extension, and on ${browser} a page inside the extension is read through its own surface relay, but that path matches none of the surface documents the manifest declares${declared.length ? ` (${declared.join(", ")})` : ""}. Script injection cannot reach an extension page at all.`
12244
+ },
12245
+ hint: declared.length ? `Pass the declared document path (extension_open surface: "${declared[0]}" opens one), or read it with extension_dom_snapshot context: "${declared[0]}".` : "Declare the page as a surface in the manifest (action.default_popup, options_ui.page, sidebar_action.default_panel or chrome_url_overrides) and rebuild."
12246
+ });
12247
+ }
12248
+ if (args.url && !surface) {
12088
12249
  const listed = await listBridgeTabs(args.projectPath, browser, args.timeout, inspect_gecko_TOOL);
12089
12250
  if ("error" in listed) return listed.error;
12090
12251
  const already = listed.tabs.some((t)=>t.url.includes(args.url));
@@ -12111,8 +12272,8 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
12111
12272
  expression,
12112
12273
  args.projectPath,
12113
12274
  "--context",
12114
- "page",
12115
- ...args.url ? [
12275
+ surface ? surface.context : "page",
12276
+ ...args.url && !surface ? [
12116
12277
  "--url",
12117
12278
  args.url
12118
12279
  ] : [],
@@ -12166,7 +12327,11 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
12166
12327
  if (null === value) return raw;
12167
12328
  const result = {
12168
12329
  browser,
12169
- transport: "bridge"
12330
+ transport: "bridge",
12331
+ ...surface ? {
12332
+ surface: surface.context,
12333
+ document: surface.document
12334
+ } : {}
12170
12335
  };
12171
12336
  if (value.meta) {
12172
12337
  result.target = {
@@ -12642,6 +12807,69 @@ async function listGeckoExtensions(projectPath, browser) {
12642
12807
  });
12643
12808
  }
12644
12809
  }
12810
+ const RELAY_MARK = "__extensionDevRelay";
12811
+ function cloneHelper() {
12812
+ return `var clone = function (v) {
12813
+ if (v === undefined) return null;
12814
+ try {
12815
+ if (typeof structuredClone === "function") { structuredClone(v); return v; }
12816
+ return JSON.parse(JSON.stringify(v));
12817
+ } catch (e) { return String(v); }
12818
+ };`;
12819
+ }
12820
+ function relaySafeExpression(expression, token) {
12821
+ const src = JSON.stringify(expression);
12822
+ const key = JSON.stringify(token);
12823
+ return `(function () {
12824
+ ${cloneHelper()}
12825
+ var value = (0, eval)(${src});
12826
+ if (value && typeof value.then === "function") {
12827
+ var store = globalThis.${RELAY_MARK} = globalThis.${RELAY_MARK} || {};
12828
+ store[${key}] = { done: false };
12829
+ Promise.resolve(value).then(function (settled) {
12830
+ store[${key}] = { done: true, ok: true, value: clone(settled) };
12831
+ }, function (error) {
12832
+ store[${key}] = { done: true, ok: false, name: (error && error.name) || "EvalError", message: (error && error.message) || String(error) };
12833
+ });
12834
+ return { ${RELAY_MARK}: 1, done: false, token: ${key} };
12835
+ }
12836
+ return { ${RELAY_MARK}: 1, done: true, ok: true, value: clone(value) };
12837
+ })()`;
12838
+ }
12839
+ function relayPollExpression(token) {
12840
+ const key = JSON.stringify(token);
12841
+ return `(function () {
12842
+ var store = globalThis.${RELAY_MARK};
12843
+ var entry = store && store[${key}];
12844
+ if (!entry) return { ${RELAY_MARK}: 1, done: true, ok: false, name: "RelayLost", message: "the page reloaded or navigated before the expression settled, so its result is gone" };
12845
+ if (!entry.done) return { ${RELAY_MARK}: 1, done: false, token: ${key} };
12846
+ delete store[${key}];
12847
+ return { ${RELAY_MARK}: 1, done: true, ok: entry.ok, value: entry.value, name: entry.name, message: entry.message };
12848
+ })()`;
12849
+ }
12850
+ function readRelayFrame(value) {
12851
+ if (!value || "object" != typeof value || Array.isArray(value)) return null;
12852
+ const frame = value;
12853
+ if (1 !== frame[RELAY_MARK] || "boolean" != typeof frame.done) return null;
12854
+ return {
12855
+ done: frame.done,
12856
+ ..."boolean" == typeof frame.ok ? {
12857
+ ok: frame.ok
12858
+ } : {},
12859
+ ..."value" in frame ? {
12860
+ value: frame.value
12861
+ } : {},
12862
+ ..."string" == typeof frame.name ? {
12863
+ name: frame.name
12864
+ } : {},
12865
+ ..."string" == typeof frame.message ? {
12866
+ message: frame.message
12867
+ } : {},
12868
+ ..."string" == typeof frame.token ? {
12869
+ token: frame.token
12870
+ } : {}
12871
+ };
12872
+ }
12645
12873
  const eval_schema = {
12646
12874
  name: "extension_eval",
12647
12875
  description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
@@ -12704,14 +12932,6 @@ function resolveDefaultEvalContext(projectPath, browser) {
12704
12932
  if (!isChromiumFamily(browser)) return "background";
12705
12933
  return 3 === chromiumManifestVersion(projectPath, browser) ? "page" : "background";
12706
12934
  }
12707
- const EXTENSION_PAGE_CONTEXTS = [
12708
- "popup",
12709
- "options",
12710
- "sidebar",
12711
- "newtab",
12712
- "history",
12713
- "bookmarks"
12714
- ];
12715
12935
  function wantsExtensionPageOverCdp(projectPath, browser, context, url) {
12716
12936
  if (!isChromiumFamily(browser) || !context) return false;
12717
12937
  if ("page" === context) return "string" == typeof url && /^chrome-extension:\/\//.test(url);
@@ -12834,6 +13054,78 @@ async function evaluateOnWebKitPage(args, browser, info) {
12834
13054
  });
12835
13055
  }
12836
13056
  }
13057
+ const RELAY_POLL_MS = 300;
13058
+ const RELAY_DEFAULT_BUDGET_MS = 30000;
13059
+ function tryParseFrame(raw) {
13060
+ try {
13061
+ const parsed = JSON.parse(raw);
13062
+ return parsed && "object" == typeof parsed ? parsed : null;
13063
+ } catch {
13064
+ return null;
13065
+ }
13066
+ }
13067
+ async function evaluateThroughRelay(args, browser, context) {
13068
+ const token = node_crypto.randomUUID();
13069
+ const budgetMs = args.timeout ?? RELAY_DEFAULT_BUDGET_MS;
13070
+ const deadline = Date.now() + budgetMs;
13071
+ const run = (expression)=>runActVerb([
13072
+ "eval",
13073
+ ...commonFlags({
13074
+ ...args,
13075
+ context,
13076
+ browser
13077
+ }),
13078
+ "--",
13079
+ expression,
13080
+ args.projectPath
13081
+ ], args.projectPath, args.timeout, eval_schema.name);
13082
+ let raw = await run(relaySafeExpression(args.expression, token));
13083
+ let parsed = tryParseFrame(raw);
13084
+ if (!parsed || true !== parsed.ok) return raw;
13085
+ let frame = readRelayFrame(parsed.value);
13086
+ if (!frame) return raw;
13087
+ let polls = 0;
13088
+ while(!frame.done){
13089
+ if (Date.now() >= deadline) return envelope_envelope({
13090
+ ok: false,
13091
+ command: eval_schema.name,
13092
+ status: "eval-pending",
13093
+ error: {
13094
+ code: "E_WAIT_TIMEOUT",
13095
+ name: "EvalPending",
13096
+ message: `The expression returned a promise that had not settled after ${budgetMs} ms; it is still running in the ${context} page.`
13097
+ },
13098
+ value: {
13099
+ context,
13100
+ token,
13101
+ polls
13102
+ },
13103
+ hint: `Its outcome lands in globalThis.${RELAY_MARK}[${JSON.stringify(token)}] inside the ${context} page when it settles ({done, ok, value}): read it with extension_eval in the same context, or pass a larger timeout to wait here.`
13104
+ });
13105
+ await new Promise((r)=>setTimeout(r, RELAY_POLL_MS));
13106
+ polls += 1;
13107
+ raw = await run(relayPollExpression(token));
13108
+ parsed = tryParseFrame(raw);
13109
+ if (!parsed || true !== parsed.ok) return raw;
13110
+ const next = readRelayFrame(parsed.value);
13111
+ if (!next) return raw;
13112
+ frame = next;
13113
+ }
13114
+ if (false === frame.ok) return envelope_envelope({
13115
+ ok: false,
13116
+ command: eval_schema.name,
13117
+ status: "eval-failed",
13118
+ error: {
13119
+ code: "E_EVAL",
13120
+ name: frame.name || "EvalError",
13121
+ message: frame.message || "the expression rejected"
13122
+ },
13123
+ hint: `The expression threw, or the promise it returned rejected, inside the ${context} page.`
13124
+ });
13125
+ parsed.value = void 0 === frame.value ? null : frame.value;
13126
+ if (polls > 0) parsed.hint = `The expression returned a promise; the ${context} page settled it and this call polled ${polls} time${1 === polls ? "" : "s"} for the result. ` + ("string" == typeof parsed.hint ? parsed.hint : "");
13127
+ return actFrameJson(parsed);
13128
+ }
12837
13129
  async function eval_handler(args) {
12838
13130
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
12839
13131
  if (WEBKIT_FAMILY.has(browser) && "page" === args.context) {
@@ -12843,6 +13135,35 @@ async function eval_handler(args) {
12843
13135
  const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
12844
13136
  const context = defaulted ? "page" : args.context;
12845
13137
  if (wantsExtensionPageOverCdp(args.projectPath, browser, context, args.url)) return evaluateOnChromiumExtensionPage(args, browser, context);
13138
+ if ("page" === context && isExtensionUrl(args.url)) {
13139
+ const surface = surfaceForExtensionUrl(args.projectPath, browser, args.url);
13140
+ if (surface) {
13141
+ const raw = await evaluateThroughRelay({
13142
+ ...args,
13143
+ url: void 0,
13144
+ tab: void 0
13145
+ }, browser, surface.context);
13146
+ const parsed = tryParseFrame(raw);
13147
+ if (parsed) {
13148
+ addWarning(parsed, `${args.url} is the extension's own ${surface.context} document (${surface.document}), which script injection cannot reach on any engine, so this evaluated through the ${surface.context} surface relay. Pass context: "${surface.context}" directly next time.`);
13149
+ return actFrameJson(parsed);
13150
+ }
13151
+ return raw;
13152
+ }
13153
+ const declared = declaredSurfaces(args.projectPath, browser) ?? [];
13154
+ return envelope_envelope({
13155
+ ok: false,
13156
+ command: eval_schema.name,
13157
+ status: "no-surface",
13158
+ error: {
13159
+ code: "E_NO_SURFACE_DOCUMENT",
13160
+ name: "NoSurfaceDocument",
13161
+ message: `${args.url} is a page inside the extension, which script injection cannot reach, and it matches none of the surface documents the manifest declares${declared.length ? ` (${declared.join(", ")})` : ""}.`
13162
+ },
13163
+ hint: declared.length ? `Evaluate in a declared surface with context: "${declared[0]}" (open it first with extension_open), or read a web page by url.` : "Declare the page as a surface in the manifest (action.default_popup, options_ui.page, sidebar_action.default_panel or chrome_url_overrides) and rebuild."
13164
+ });
13165
+ }
13166
+ if (context && EXTENSION_PAGE_CONTEXTS.includes(context)) return evaluateThroughRelay(args, browser, context);
12846
13167
  const raw = await runActVerb([
12847
13168
  "eval",
12848
13169
  ...commonFlags({
@@ -14531,6 +14852,12 @@ async function wait_handler(args) {
14531
14852
  pid: contract.pid,
14532
14853
  distPath: contract.distPath,
14533
14854
  manifestPath: contract.manifestPath,
14855
+ ..."number" == typeof contract.browserPid ? {
14856
+ browserPid: contract.browserPid
14857
+ } : {},
14858
+ ..."string" == typeof contract.profilePath && contract.profilePath ? {
14859
+ profilePath: contract.profilePath
14860
+ } : {},
14534
14861
  compiledAt: contract.compiledAt,
14535
14862
  startedAt: contract.startedAt,
14536
14863
  budgetMs,
@@ -15387,44 +15714,58 @@ function resolveCacheRoot() {
15387
15714
  if (home) return node_path.join(home, ".cache", "extension.js", "browsers");
15388
15715
  return node_path.resolve(process.cwd(), ".cache", "extension.js", "browsers");
15389
15716
  }
15390
- function findManagedBinary(browser) {
15391
- const browserDir = node_path.join(resolveCacheRoot(), browser);
15717
+ const MANAGED_SEARCH_DEPTH = 8;
15718
+ const MANAGED_EXEC_NAMES = {
15719
+ chrome: [
15720
+ "chrome",
15721
+ "chrome.exe",
15722
+ "Google Chrome for Testing"
15723
+ ],
15724
+ chromium: [
15725
+ "chrome",
15726
+ "chromium",
15727
+ "chrome.exe",
15728
+ "chromium.exe",
15729
+ "Chromium"
15730
+ ],
15731
+ edge: [
15732
+ "msedge",
15733
+ "msedge.exe",
15734
+ "microsoft-edge",
15735
+ "Microsoft Edge"
15736
+ ],
15737
+ firefox: [
15738
+ "firefox",
15739
+ "firefox.exe",
15740
+ "firefox-bin"
15741
+ ]
15742
+ };
15743
+ function executableInsideBundle(bundle, names) {
15744
+ const macos = node_path.join(bundle, "Contents", "MacOS");
15745
+ try {
15746
+ for (const entry of node_fs.readdirSync(macos, {
15747
+ withFileTypes: true
15748
+ }))if (entry.isFile() && names.includes(entry.name)) return node_path.join(macos, entry.name);
15749
+ } catch {}
15750
+ return null;
15751
+ }
15752
+ function findManagedBinary(browser, cacheRoot = resolveCacheRoot()) {
15753
+ const browserDir = node_path.join(cacheRoot, browser);
15392
15754
  if (!node_fs.existsSync(browserDir)) return null;
15393
- const execNames = {
15394
- chrome: [
15395
- "chrome",
15396
- "chrome.exe",
15397
- "Google Chrome for Testing"
15398
- ],
15399
- chromium: [
15400
- "chrome",
15401
- "chromium",
15402
- "chrome.exe",
15403
- "chromium.exe",
15404
- "Chromium.app"
15405
- ],
15406
- edge: [
15407
- "msedge",
15408
- "msedge.exe",
15409
- "microsoft-edge",
15410
- "Microsoft Edge"
15411
- ],
15412
- firefox: [
15413
- "firefox",
15414
- "firefox.exe",
15415
- "Firefox.app"
15416
- ]
15417
- };
15418
- const names = execNames[browser] ?? [];
15755
+ const names = MANAGED_EXEC_NAMES[browser] ?? [];
15419
15756
  function search(dir, depth) {
15420
- if (depth > 4) return null;
15757
+ if (depth > MANAGED_SEARCH_DEPTH) return null;
15421
15758
  try {
15422
15759
  for (const entry of node_fs.readdirSync(dir, {
15423
15760
  withFileTypes: true
15424
15761
  })){
15425
15762
  const full = node_path.join(dir, entry.name);
15426
15763
  if (entry.isFile() && names.includes(entry.name)) return full;
15427
- if (entry.isDirectory() && depth < 4) {
15764
+ if (entry.isDirectory() && entry.name.endsWith(".app")) {
15765
+ const inside = executableInsideBundle(full, names);
15766
+ if (inside) return inside;
15767
+ }
15768
+ if (entry.isDirectory() && depth < MANAGED_SEARCH_DEPTH) {
15428
15769
  const found = search(full, depth + 1);
15429
15770
  if (found) return found;
15430
15771
  }
@@ -15475,14 +15816,16 @@ async function detectBrowsers(browsers) {
15475
15816
  const isWebkit = WEBKIT_FAMILY.has(browser);
15476
15817
  let binaryPath = findManagedBinary(browser);
15477
15818
  let source = "managed";
15819
+ const systemBinaryPath = findSystemBinary(browser);
15478
15820
  if (!binaryPath) {
15479
- binaryPath = findSystemBinary(browser);
15821
+ binaryPath = systemBinaryPath;
15480
15822
  source = binaryPath ? "system" : "not_found";
15481
15823
  }
15482
15824
  let version = null;
15483
15825
  if (binaryPath && !isWebkit) version = await getVersion(binaryPath, browser);
15484
15826
  else if (binaryPath && isWebkit) version = await readSafariVersion(binaryPath);
15485
15827
  const automation = isWebkit && binaryPath ? await detectSafariAutomation(binaryPath) : null;
15828
+ const shadowedSystem = "managed" === source && systemBinaryPath && systemBinaryPath !== binaryPath ? systemBinaryPath : null;
15486
15829
  detected.push({
15487
15830
  browser,
15488
15831
  binaryPath,
@@ -15493,6 +15836,10 @@ async function detectBrowsers(browsers) {
15493
15836
  rdpSupport: isGecko,
15494
15837
  ...automation ? {
15495
15838
  automation
15839
+ } : {},
15840
+ ...shadowedSystem ? {
15841
+ systemBinaryPath: shadowedSystem,
15842
+ devLaunches: `extension_dev launches the managed binary above, not the system install at ${shadowedSystem}; pass ${isGecko ? "geckoBinary" : "chromiumBinary"} to extension_dev to use the system one.`
15496
15843
  } : {}
15497
15844
  });
15498
15845
  }
@@ -0,0 +1,7 @@
1
+ export declare const EXTENSION_PAGE_CONTEXTS: string[];
2
+ export declare const EXTENSION_ORIGIN: RegExp;
3
+ export declare function isExtensionUrl(url: string | undefined): boolean;
4
+ export declare function surfaceForExtensionUrl(projectPath: string, browser: string, url: string): {
5
+ context: string;
6
+ document: string;
7
+ } | null;
@@ -0,0 +1,12 @@
1
+ export declare const RELAY_MARK = "__extensionDevRelay";
2
+ export interface RelayFrame {
3
+ done: boolean;
4
+ ok?: boolean;
5
+ value?: unknown;
6
+ name?: string;
7
+ message?: string;
8
+ token?: string;
9
+ }
10
+ export declare function relaySafeExpression(expression: string, token: string): string;
11
+ export declare function relayPollExpression(token: string): string;
12
+ export declare function readRelayFrame(value: unknown): RelayFrame | null;
@@ -65,6 +65,9 @@ export interface ReadyContract {
65
65
  compiledAt?: string | null;
66
66
  executorAttachedAt?: string;
67
67
  runtime?: string;
68
+ profilePath?: string;
69
+ browserPid?: number | null;
70
+ launcherPid?: number;
68
71
  }
69
72
  export interface ProcessInfo {
70
73
  pid: number;
@@ -1 +1,2 @@
1
+ export declare function findManagedBinary(browser: string, cacheRoot?: string): string | null;
1
2
  export declare function detectBrowsers(browsers?: string[]): Promise<string>;
@@ -1,4 +1,5 @@
1
1
  import { type ActArgs } from "../lib/act";
2
+ import { EXTENSION_PAGE_CONTEXTS } from "../lib/extension-surfaces";
2
3
  export declare const schema: {
3
4
  name: string;
4
5
  description: string;
@@ -40,7 +41,7 @@ export declare const schema: {
40
41
  };
41
42
  export declare function chromiumManifestVersion(projectPath: string, browser: string): 2 | 3 | null;
42
43
  export declare function resolveDefaultEvalContext(projectPath: string, browser: string): "background" | "page";
43
- export declare const EXTENSION_PAGE_CONTEXTS: string[];
44
+ export { EXTENSION_PAGE_CONTEXTS };
44
45
  export declare function wantsExtensionPageOverCdp(projectPath: string, browser: string, context: string | undefined, url: string | undefined): boolean;
45
46
  export declare function handler(args: ActArgs & {
46
47
  expression: string;
@@ -1,3 +1,5 @@
1
+ import { surfaceForExtensionUrl } from "../lib/extension-surfaces";
2
+ export { surfaceForExtensionUrl };
1
3
  export declare function inspectViaBridge(args: {
2
4
  projectPath: string;
3
5
  url?: string;
@@ -30,6 +30,11 @@ interface StopOutcome {
30
30
  detail: string;
31
31
  carrierRemoved?: string;
32
32
  }
33
+ export interface ContractProcessHints {
34
+ profilePath?: string;
35
+ pids: number[];
36
+ }
37
+ export declare function contractProcessHints(projectPath: string, browser: string): ContractProcessHints;
33
38
  export declare function stopOne(projectPath: string, browser: string): Promise<StopOutcome>;
34
39
  export declare function handler(args: {
35
40
  projectPath?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "10.10.0",
4
+ "version": "10.10.1",
5
5
  "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 30 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
6
6
  "mcpName": "io.github.extensiondev/mcp",
7
7
  "license": "Apache-2.0",
@@ -91,9 +91,9 @@
91
91
  "@modelcontextprotocol/sdk": "^1.12.1",
92
92
  "browser-extension-manifest-fields": "^2.2.9",
93
93
  "cross-spawn": "^7.0.6",
94
- "extension-create": "4.1.29",
95
- "extension-develop": "4.1.29",
96
- "extension-install": "4.1.29",
94
+ "extension-create": "4.1.30",
95
+ "extension-develop": "4.1.30",
96
+ "extension-install": "4.1.30",
97
97
  "ws": "^8.20.0"
98
98
  },
99
99
  "devDependencies": {
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "10.10.0",
10
+ "version": "10.10.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@extension.dev/mcp",
16
- "version": "10.10.0",
16
+ "version": "10.10.1",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"