@extension.dev/mcp 4.7.0 → 4.8.0

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 60+ 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": "4.7.0",
13
+ "version": "4.8.0",
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 60+ 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": "4.7.0",
4
+ "version": "4.8.0",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "boss@cezaraugusto.net",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.8.0
4
+
5
+ Dev-session ergonomics hardened from a 30-persona agent walk of the toolchain.
6
+
7
+ - **`allowEval` now implies `allowControl`.** Enabling eval on `extension_dev`
8
+ also opens the control channel, so a single `allowEval: true` unlocks
9
+ `extension_storage`/`reload`/`open`/`dom_inspect` too. `extension_dev` now
10
+ returns a `capabilities` block naming exactly which verbs the session unlocked,
11
+ ending the stop-and-restart loop that hit agents who passed one flag and not
12
+ the other.
13
+ - **Session-aware browser default.** `extension_stop` (and the other
14
+ browser-scoped tools) resolve the browser from the one live session for the
15
+ project instead of assuming `chrome`. `extension_stop` also reaps the launched
16
+ browser's process tree and refuses to report `stopped: true` while a process
17
+ survives, fixing orphaned browsers (notably Firefox) after a stop.
18
+ - **Forgiving argument names.** Common synonyms are accepted and normalized:
19
+ `path`/`dir` for `projectPath`, `name` for `projectName`, `template` for
20
+ `slug`, `code` for `expression`, and more, so a reasonable first guess no
21
+ longer 400s.
22
+ - **`extension_manifest_validate`** accepts `projectPath` (it finds the
23
+ manifest) and probes path-valued fields (popup, service worker, icons, content
24
+ scripts) against disk, warning on dangling references instead of a false
25
+ all-clear.
26
+ - **`extension_doctor`** inlines the dev session's own recorded errors so a build
27
+ or load failure no longer reads as healthy.
28
+ - **`extension_inspect`** lists declared entrypoints (so a small content script
29
+ is not buried under assets) and warns when a store-listing promo image is
30
+ shipped inside the package.
31
+ - **`extension_source_inspect`** on a Gecko session now names the working
32
+ alternatives (`extension_logs`, `extension_eval`) instead of pointing back at
33
+ the tool that just refused.
34
+
3
35
  ## 4.7.0
4
36
 
5
37
  `extension_deploy` now submits **through** extension.dev instead of driving a
package/dist/module.js CHANGED
@@ -7,11 +7,11 @@ import node_path, { join } from "node:path";
7
7
  import { extensionCreate } from "extension-create";
8
8
  import node_os from "node:os";
9
9
  import cross_spawn from "cross-spawn";
10
+ import { execFile, execFileSync } from "node:child_process";
10
11
  import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
11
12
  import ws from "ws";
12
13
  import node_http from "node:http";
13
14
  import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
14
- import { execFile } from "node:child_process";
15
15
  import { promisify } from "node:util";
16
16
  var add_feature_namespaceObject = {};
17
17
  __webpack_require__.r(add_feature_namespaceObject);
@@ -199,7 +199,7 @@ __webpack_require__.d(whoami_namespaceObject, {
199
199
  handler: ()=>whoami_handler,
200
200
  schema: ()=>whoami_schema
201
201
  });
202
- var package_namespaceObject = JSON.parse('{"rE":"4.6.0","El":{"OP":"^4.0.11"}}');
202
+ var package_namespaceObject = JSON.parse('{"rE":"4.7.0","El":{"OP":"^4.0.11"}}');
203
203
  function detectPackageManager(projectPath) {
204
204
  const byLockfile = [
205
205
  [
@@ -769,7 +769,7 @@ const dev_schema = {
769
769
  allowEval: {
770
770
  type: "boolean",
771
771
  default: false,
772
- description: "Additionally enable extension_eval (runs code in a context; writes a 0600 session token)"
772
+ description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect — you do not need to pass both."
773
773
  }
774
774
  },
775
775
  required: [
@@ -779,6 +779,7 @@ const dev_schema = {
779
779
  };
780
780
  async function dev_handler(args) {
781
781
  const browser = args.browser ?? "chrome";
782
+ const allowControl = Boolean(args.allowControl || args.allowEval);
782
783
  const cliArgs = [
783
784
  "dev",
784
785
  args.projectPath,
@@ -789,7 +790,7 @@ async function dev_handler(args) {
789
790
  if (args.noBrowser) cliArgs.push("--no-browser");
790
791
  if (false === args.polyfill) cliArgs.push("--polyfill", "false");
791
792
  cliArgs.push(...launchFlagArgs(args));
792
- if (args.allowControl) cliArgs.push("--allow-control");
793
+ if (allowControl) cliArgs.push("--allow-control");
793
794
  if (args.allowEval) cliArgs.push("--allow-eval");
794
795
  const child = spawnExtensionCli(cliArgs, {
795
796
  projectDir: args.projectPath
@@ -812,13 +813,20 @@ async function dev_handler(args) {
812
813
  await new Promise((resolve)=>setTimeout(resolve, 3000));
813
814
  child.stdout?.off("data", collector);
814
815
  child.stderr?.off("data", collector);
816
+ const controlVerbs = "storage, reload, open, dom_inspect";
817
+ const capabilities = {
818
+ allowControl,
819
+ allowEval: Boolean(args.allowEval),
820
+ unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
821
+ };
815
822
  return JSON.stringify({
816
823
  pid,
817
824
  browser,
818
825
  port: args.port ?? 8080,
819
826
  projectPath: args.projectPath,
820
827
  status: "started",
821
- hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. When you are done, call extension_stop to shut down the dev server and browser.",
828
+ capabilities,
829
+ hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). Restart extension_dev with the flag you need.") + " When you are done, call extension_stop to shut down the dev server and browser.",
822
830
  earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500)
823
831
  });
824
832
  }
@@ -1003,6 +1011,66 @@ async function preview_handler(args) {
1003
1011
  hint: "Call extension_stop when you are done to close the preview browser."
1004
1012
  });
1005
1013
  }
1014
+ function contractSightings(projectPath) {
1015
+ const root = node_path.resolve(projectPath, "dist", "extension-js");
1016
+ let dirs;
1017
+ try {
1018
+ dirs = node_fs.readdirSync(root);
1019
+ } catch {
1020
+ return [];
1021
+ }
1022
+ const sightings = [];
1023
+ for (const dir of dirs){
1024
+ const readyPath = node_path.join(root, dir, "ready.json");
1025
+ try {
1026
+ const stat = node_fs.statSync(readyPath);
1027
+ const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
1028
+ if (contract?.status !== "ready") continue;
1029
+ sightings.push({
1030
+ browser: dir,
1031
+ mtimeMs: stat.mtimeMs,
1032
+ pid: "number" == typeof contract.pid ? contract.pid : void 0
1033
+ });
1034
+ } catch {}
1035
+ }
1036
+ return sightings;
1037
+ }
1038
+ function pidAlive(pid) {
1039
+ try {
1040
+ process.kill(pid, 0);
1041
+ return true;
1042
+ } catch {
1043
+ return false;
1044
+ }
1045
+ }
1046
+ function knownSessionBrowsers(projectPath) {
1047
+ const resolved = node_path.resolve(projectPath);
1048
+ const browsers = [];
1049
+ for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
1050
+ for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
1051
+ return Array.from(new Set(browsers));
1052
+ }
1053
+ function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
1054
+ if (explicit) return {
1055
+ browser: explicit,
1056
+ source: "explicit"
1057
+ };
1058
+ const resolved = node_path.resolve(projectPath);
1059
+ const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
1060
+ if (mine.length > 0) return {
1061
+ browser: mine[mine.length - 1].browser,
1062
+ source: "session"
1063
+ };
1064
+ const sightings = contractSightings(projectPath).filter((s)=>void 0 === s.pid || pidAlive(s.pid)).sort((a, b)=>b.mtimeMs - a.mtimeMs);
1065
+ if (sightings.length > 0) return {
1066
+ browser: sightings[0].browser,
1067
+ source: "contract"
1068
+ };
1069
+ return {
1070
+ browser: fallback,
1071
+ source: "fallback"
1072
+ };
1073
+ }
1006
1074
  const stop_schema = {
1007
1075
  name: "extension_stop",
1008
1076
  description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
@@ -1015,8 +1083,7 @@ const stop_schema = {
1015
1083
  },
1016
1084
  browser: {
1017
1085
  type: "string",
1018
- default: "chrome",
1019
- description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start)"
1086
+ description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start). Defaults to the one live session for this project when omitted, instead of assuming chrome."
1020
1087
  },
1021
1088
  all: {
1022
1089
  type: "boolean",
@@ -1027,6 +1094,27 @@ const stop_schema = {
1027
1094
  required: []
1028
1095
  }
1029
1096
  };
1097
+ function profileProcessPids(projectPath, browser) {
1098
+ const marker = node_path.resolve(projectPath, "dist", `extension-profile-${browser}`);
1099
+ try {
1100
+ const out = execFileSync("pgrep", [
1101
+ "-f",
1102
+ marker
1103
+ ], {
1104
+ encoding: "utf8"
1105
+ });
1106
+ return out.split("\n").map((s)=>parseInt(s.trim(), 10)).filter((n)=>Number.isInteger(n) && n > 0 && n !== process.pid);
1107
+ } catch {
1108
+ return [];
1109
+ }
1110
+ }
1111
+ function reapProfileProcesses(projectPath, browser) {
1112
+ const pids = profileProcessPids(projectPath, browser);
1113
+ for (const pid of pids)try {
1114
+ process.kill(pid, "SIGKILL");
1115
+ } catch {}
1116
+ return pids;
1117
+ }
1030
1118
  function isAlive(pid) {
1031
1119
  try {
1032
1120
  process.kill(pid, 0);
@@ -1063,13 +1151,17 @@ function pidFromReadyContract(projectPath, browser) {
1063
1151
  async function stopOne(projectPath, browser) {
1064
1152
  const session = getSession(projectPath, browser);
1065
1153
  const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
1066
- if (null == pid) return {
1067
- projectPath,
1068
- browser,
1069
- pid: null,
1070
- stopped: false,
1071
- detail: "No known session for this project/browser (nothing registered in this server and no ready.json contract found)."
1072
- };
1154
+ if (null == pid) {
1155
+ const reaped = reapProfileProcesses(projectPath, browser);
1156
+ return {
1157
+ projectPath,
1158
+ browser,
1159
+ pid: null,
1160
+ stopped: 0 !== reaped.length,
1161
+ reaped,
1162
+ detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
1163
+ };
1164
+ }
1073
1165
  let detail;
1074
1166
  if (isAlive(pid)) {
1075
1167
  signal(pid, "SIGTERM");
@@ -1080,17 +1172,23 @@ async function stopOne(projectPath, browser) {
1080
1172
  }
1081
1173
  detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
1082
1174
  } else detail = "Process was already gone; cleaned up session records.";
1175
+ const reaped = reapProfileProcesses(projectPath, browser);
1083
1176
  removeSession(projectPath, browser);
1084
1177
  try {
1085
1178
  node_fs.rmSync(readyJsonPath(projectPath, browser), {
1086
1179
  force: true
1087
1180
  });
1088
1181
  } catch {}
1182
+ const survivors = profileProcessPids(projectPath, browser);
1183
+ const stopped = !isAlive(pid) && 0 === survivors.length;
1184
+ if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
1185
+ else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
1089
1186
  return {
1090
1187
  projectPath,
1091
1188
  browser,
1092
1189
  pid,
1093
- stopped: !isAlive(pid),
1190
+ stopped,
1191
+ reaped,
1094
1192
  detail
1095
1193
  };
1096
1194
  }
@@ -1110,7 +1208,8 @@ async function stop_handler(args) {
1110
1208
  if (!args.projectPath) return JSON.stringify({
1111
1209
  error: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
1112
1210
  });
1113
- const outcome = await stopOne(args.projectPath, args.browser ?? "chrome");
1211
+ const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
1212
+ const outcome = await stopOne(args.projectPath, browser);
1114
1213
  return JSON.stringify(outcome);
1115
1214
  }
1116
1215
  const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
@@ -1298,7 +1397,11 @@ const manifest_validate_schema = {
1298
1397
  properties: {
1299
1398
  manifestPath: {
1300
1399
  type: "string",
1301
- description: "Path to manifest.json"
1400
+ description: "Path to manifest.json. Or pass projectPath and the manifest is located for you."
1401
+ },
1402
+ projectPath: {
1403
+ type: "string",
1404
+ description: "Path to the extension project root; manifest.json is resolved from it (root or src/). Accepted in place of manifestPath."
1302
1405
  },
1303
1406
  browsers: {
1304
1407
  type: "array",
@@ -1312,11 +1415,64 @@ const manifest_validate_schema = {
1312
1415
  description: "Browsers to validate against"
1313
1416
  }
1314
1417
  },
1315
- required: [
1316
- "manifestPath"
1317
- ]
1418
+ required: []
1318
1419
  }
1319
1420
  };
1421
+ function collectPathRefs(m) {
1422
+ const refs = [];
1423
+ const push = (v)=>{
1424
+ if ("string" == typeof v) refs.push(v);
1425
+ };
1426
+ const action = m.action || m.browser_action;
1427
+ if (action) {
1428
+ push(action.default_popup);
1429
+ if ("string" == typeof action.default_icon) push(action.default_icon);
1430
+ else if (action.default_icon) Object.values(action.default_icon).forEach(push);
1431
+ }
1432
+ const bg = m.background;
1433
+ if (bg) {
1434
+ push(bg.service_worker);
1435
+ push(bg.page);
1436
+ if (Array.isArray(bg.scripts)) bg.scripts.forEach(push);
1437
+ }
1438
+ if (m.icons) Object.values(m.icons).forEach(push);
1439
+ const cs = m.content_scripts;
1440
+ if (Array.isArray(cs)) for (const c of cs){
1441
+ if (Array.isArray(c.js)) c.js.forEach(push);
1442
+ if (Array.isArray(c.css)) c.css.forEach(push);
1443
+ }
1444
+ push(m.options_page);
1445
+ const oui = m.options_ui;
1446
+ if (oui) push(oui.page);
1447
+ const sp = m.side_panel;
1448
+ if (sp) push(sp.default_path);
1449
+ const sa = m.sidebar_action;
1450
+ if (sa) push(sa.default_panel);
1451
+ const cuo = m.chrome_url_overrides;
1452
+ if (cuo) Object.values(cuo).forEach(push);
1453
+ return refs;
1454
+ }
1455
+ function fileResolvesSomewhere(ref, roots) {
1456
+ if (!ref || ref.includes("*") || /^(https?:|data:)/i.test(ref)) return true;
1457
+ const clean = ref.replace(/^\.?\//, "");
1458
+ return roots.some((root)=>{
1459
+ try {
1460
+ return node_fs.existsSync(node_path.resolve(root, clean));
1461
+ } catch {
1462
+ return false;
1463
+ }
1464
+ });
1465
+ }
1466
+ function findManifest(projectPath) {
1467
+ for (const rel of [
1468
+ "manifest.json",
1469
+ node_path.join("src", "manifest.json")
1470
+ ]){
1471
+ const candidate = node_path.resolve(projectPath, rel);
1472
+ if (node_fs.existsSync(candidate)) return candidate;
1473
+ }
1474
+ return null;
1475
+ }
1320
1476
  async function manifest_validate_handler(args) {
1321
1477
  const browsers = args.browsers ?? [
1322
1478
  "chrome",
@@ -1329,9 +1485,20 @@ async function manifest_validate_handler(args) {
1329
1485
  browserSupport: {},
1330
1486
  similarTemplates: []
1331
1487
  };
1488
+ const manifestPath = args.manifestPath ?? (args.projectPath ? findManifest(args.projectPath) : null);
1489
+ if (!manifestPath) return JSON.stringify({
1490
+ valid: false,
1491
+ errors: [
1492
+ args.projectPath ? `No manifest.json found under ${args.projectPath} (looked in the root and src/).` : "Pass manifestPath (path to manifest.json) or projectPath (project root)."
1493
+ ],
1494
+ warnings: [],
1495
+ browserSupport: {},
1496
+ similarTemplates: []
1497
+ });
1498
+ const manifestDir = node_path.dirname(node_path.resolve(manifestPath));
1332
1499
  let manifest;
1333
1500
  try {
1334
- const raw = node_fs.readFileSync(node_path.resolve(args.manifestPath), "utf8");
1501
+ const raw = node_fs.readFileSync(node_path.resolve(manifestPath), "utf8");
1335
1502
  manifest = JSON.parse(raw);
1336
1503
  } catch (err) {
1337
1504
  return JSON.stringify({
@@ -1347,6 +1514,14 @@ async function manifest_validate_handler(args) {
1347
1514
  if (!manifest.name) result.errors.push("Missing required field: name");
1348
1515
  if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
1349
1516
  const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
1517
+ const roots = [
1518
+ manifestDir,
1519
+ node_path.join(manifestDir, "src"),
1520
+ ..."src" === node_path.basename(manifestDir) ? [
1521
+ node_path.dirname(manifestDir)
1522
+ ] : []
1523
+ ];
1524
+ for (const ref of new Set(collectPathRefs(chromiumManifest)))if (!fileResolvesSomewhere(ref, roots)) result.warnings.push(`Referenced file "${ref}" was not found near the manifest — this is the kind of dangling reference extension_build fails on. Verify with extension_build.`);
1350
1525
  if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
1351
1526
  const declaredPerms = [
1352
1527
  ...chromiumManifest.permissions ?? [],
@@ -1525,9 +1700,46 @@ async function inspect_handler(args) {
1525
1700
  const sourcemapSize = byType.sourcemap?.size ?? 0;
1526
1701
  const buildType = sourcemapSize > 0 ? "development" : "production";
1527
1702
  const shippableSize = totalSize - sourcemapSize;
1703
+ const sizeByPath = new Map(files.map((f)=>[
1704
+ f.path,
1705
+ f.size
1706
+ ]));
1707
+ const entrypoints = [];
1708
+ const addEntry = (role, ref)=>{
1709
+ if ("string" != typeof ref) return;
1710
+ const size = sizeByPath.get(ref.replace(/^\.?\//, ""));
1711
+ entrypoints.push({
1712
+ role,
1713
+ path: ref,
1714
+ present: void 0 !== size,
1715
+ ...void 0 !== size ? {
1716
+ sizeFormatted: formatBytes(size)
1717
+ } : {}
1718
+ });
1719
+ };
1720
+ const bg = manifest.background;
1721
+ if (bg?.service_worker) addEntry("background.service_worker", bg.service_worker);
1722
+ if (Array.isArray(bg?.scripts)) bg.scripts.forEach((s)=>addEntry("background.scripts", s));
1723
+ const actionField = manifest.action || manifest.browser_action;
1724
+ if (actionField?.default_popup) addEntry("action.default_popup", actionField.default_popup);
1725
+ const contentScripts = manifest.content_scripts;
1726
+ if (Array.isArray(contentScripts)) contentScripts.forEach((c, i)=>{
1727
+ if (Array.isArray(c.js)) c.js.forEach((j)=>addEntry(`content_scripts[${i}].js`, j));
1728
+ if (Array.isArray(c.css)) c.css.forEach((s)=>addEntry(`content_scripts[${i}].css`, s));
1729
+ });
1730
+ const PROMO_RE = /(screenshot|promo|marquee|tile|banner|preview)[-_.]?\d*\.(png|jpe?g|webp|gif)$/i;
1731
+ const sizeWarnings = [];
1732
+ for (const f of files)if ("sourcemap" !== f.type) {
1733
+ if (PROMO_RE.test(f.path)) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) looks like a store-listing promo image shipped inside the extension package — move it out of the bundled sources so it does not inflate the store zip.`);
1734
+ else if ("image" === f.type && !f.path.includes("icon") && f.size > 51200 && shippableSize > 0 && f.size / shippableSize > 0.25) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) is ${Math.round(f.size / shippableSize * 100)}% of the shipped bundle — unusually large for a shipped asset.`);
1735
+ }
1528
1736
  const result = {
1529
1737
  browser,
1530
1738
  distPath,
1739
+ entrypoints,
1740
+ ...sizeWarnings.length ? {
1741
+ sizeWarnings
1742
+ } : {},
1531
1743
  buildType,
1532
1744
  totalSize,
1533
1745
  totalSizeFormatted: formatBytes(totalSize),
@@ -1567,6 +1779,7 @@ async function inspect_handler(args) {
1567
1779
  hasManifest: node_fs.existsSync(manifestPath),
1568
1780
  hasIcons: files.some((f)=>"image" === f.type && f.path.includes("icon")),
1569
1781
  noSourceMaps: !files.some((f)=>"sourcemap" === f.type),
1782
+ noPromoAssets: !files.some((f)=>PROMO_RE.test(f.path)),
1570
1783
  under10MB: totalSize < 10485760
1571
1784
  }
1572
1785
  };
@@ -2027,66 +2240,6 @@ function isCdpEndpoint(port) {
2027
2240
  });
2028
2241
  });
2029
2242
  }
2030
- function contractSightings(projectPath) {
2031
- const root = node_path.resolve(projectPath, "dist", "extension-js");
2032
- let dirs;
2033
- try {
2034
- dirs = node_fs.readdirSync(root);
2035
- } catch {
2036
- return [];
2037
- }
2038
- const sightings = [];
2039
- for (const dir of dirs){
2040
- const readyPath = node_path.join(root, dir, "ready.json");
2041
- try {
2042
- const stat = node_fs.statSync(readyPath);
2043
- const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
2044
- if (contract?.status !== "ready") continue;
2045
- sightings.push({
2046
- browser: dir,
2047
- mtimeMs: stat.mtimeMs,
2048
- pid: "number" == typeof contract.pid ? contract.pid : void 0
2049
- });
2050
- } catch {}
2051
- }
2052
- return sightings;
2053
- }
2054
- function pidAlive(pid) {
2055
- try {
2056
- process.kill(pid, 0);
2057
- return true;
2058
- } catch {
2059
- return false;
2060
- }
2061
- }
2062
- function knownSessionBrowsers(projectPath) {
2063
- const resolved = node_path.resolve(projectPath);
2064
- const browsers = [];
2065
- for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
2066
- for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
2067
- return Array.from(new Set(browsers));
2068
- }
2069
- function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
2070
- if (explicit) return {
2071
- browser: explicit,
2072
- source: "explicit"
2073
- };
2074
- const resolved = node_path.resolve(projectPath);
2075
- const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
2076
- if (mine.length > 0) return {
2077
- browser: mine[mine.length - 1].browser,
2078
- source: "session"
2079
- };
2080
- const sightings = contractSightings(projectPath).filter((s)=>void 0 === s.pid || pidAlive(s.pid)).sort((a, b)=>b.mtimeMs - a.mtimeMs);
2081
- if (sightings.length > 0) return {
2082
- browser: sightings[0].browser,
2083
- source: "contract"
2084
- };
2085
- return {
2086
- browser: fallback,
2087
- source: "fallback"
2088
- };
2089
- }
2090
2243
  const source_inspect_schema = {
2091
2244
  name: "extension_source_inspect",
2092
2245
  description: "Inspect a running extension's live state via Chrome DevTools Protocol: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Requires an active dev or start session.",
@@ -2157,8 +2310,8 @@ async function source_inspect_handler(args) {
2157
2310
  ]);
2158
2311
  const maxBytes = args.maxBytes ?? 262144;
2159
2312
  if (!isChromiumFamily(browser)) return JSON.stringify({
2160
- error: `Source inspection for ${browser} uses RDP (Remote Debug Protocol). Currently only Chromium CDP is supported.`,
2161
- hint: 'Pass browser: "chrome" (against a Chromium-family dev session).'
2313
+ error: `Source inspection reads the live DOM over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. This is a capability limit, not a missing session.`,
2314
+ hint: `For the ${browser} session, read runtime state with extension_logs, or extension_eval (context: "content"/"page") which works against Firefox over the control channel. To get CDP DOM inspection, run a Chromium-family dev session (extension_dev with browser: "chrome") in parallel.`
2162
2315
  });
2163
2316
  const resolved = await resolveCdpPort(args.projectPath, browser);
2164
2317
  if (!resolved) return JSON.stringify({
@@ -4711,6 +4864,14 @@ async function detect_browsers_handler(args) {
4711
4864
  hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_install_browser to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` : "All requested browsers are available."
4712
4865
  });
4713
4866
  }
4867
+ function doctor_readReadyContract(projectPath, browser) {
4868
+ try {
4869
+ const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
4870
+ return JSON.parse(raw);
4871
+ } catch {
4872
+ return null;
4873
+ }
4874
+ }
4714
4875
  const doctor_schema = {
4715
4876
  name: "extension_doctor",
4716
4877
  description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, and browser liveness. Returns one {check, status, detail, remediation?} entry per leg in dependency order — a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Wraps `extension doctor`. Call with no projectPath for a pre-flight environment check (node, extension CLI, template cache) before any project exists.",
@@ -4786,9 +4947,21 @@ async function doctor_handler(args) {
4786
4947
  if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
4787
4948
  if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
4788
4949
  }
4950
+ let healthy = 0 === code;
4951
+ const contract = doctor_readReadyContract(projectPath, browser);
4952
+ if (contract?.status === "error") {
4953
+ healthy = false;
4954
+ const detail = contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
4955
+ checks.push({
4956
+ check: "runtime-errors",
4957
+ status: "fail",
4958
+ detail: toMcpSpeak(detail),
4959
+ remediation: "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
4960
+ });
4961
+ }
4789
4962
  return JSON.stringify({
4790
4963
  browser,
4791
- healthy: 0 === code,
4964
+ healthy,
4792
4965
  checks
4793
4966
  });
4794
4967
  } catch {
@@ -4835,6 +5008,55 @@ function checkPrimitive(path, value, schema, issues) {
4835
5008
  });
4836
5009
  }
4837
5010
  }
5011
+ const ARG_ALIASES = {
5012
+ projectPath: [
5013
+ "path",
5014
+ "dir",
5015
+ "projectDir",
5016
+ "cwd"
5017
+ ],
5018
+ projectName: [
5019
+ "name"
5020
+ ],
5021
+ parentDir: [
5022
+ "parent",
5023
+ "into"
5024
+ ],
5025
+ slug: [
5026
+ "template",
5027
+ "templateSlug"
5028
+ ],
5029
+ expression: [
5030
+ "code",
5031
+ "js",
5032
+ "script"
5033
+ ],
5034
+ manifestPath: [
5035
+ "manifest"
5036
+ ],
5037
+ surface: [
5038
+ "view"
5039
+ ]
5040
+ };
5041
+ function normalizeArgAliases(inputSchema, args) {
5042
+ const schema = inputSchema;
5043
+ const props = schema.properties ?? {};
5044
+ const out = {
5045
+ ...args
5046
+ };
5047
+ for (const [canonical, aliases] of Object.entries(ARG_ALIASES))if (canonical in props) {
5048
+ if (void 0 === out[canonical]) {
5049
+ for (const alias of aliases)if (!(alias in props)) {
5050
+ if (void 0 !== out[alias]) {
5051
+ out[canonical] = out[alias];
5052
+ delete out[alias];
5053
+ break;
5054
+ }
5055
+ }
5056
+ }
5057
+ }
5058
+ return out;
5059
+ }
4838
5060
  function validateToolInput(inputSchema, args) {
4839
5061
  const schema = inputSchema;
4840
5062
  const issues = [];
@@ -4933,7 +5155,8 @@ async function startServer() {
4933
5155
  ],
4934
5156
  isError: true
4935
5157
  };
4936
- const issues = validateToolInput(tool.schema.inputSchema, args ?? {});
5158
+ const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
5159
+ const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
4937
5160
  if (issues.length) return {
4938
5161
  content: [
4939
5162
  {
@@ -4944,7 +5167,7 @@ async function startServer() {
4944
5167
  isError: true
4945
5168
  };
4946
5169
  try {
4947
- const result = await tool.handler(args ?? {});
5170
+ const result = await tool.handler(normalizedArgs);
4948
5171
  return {
4949
5172
  content: [
4950
5173
  {
@@ -2,5 +2,6 @@ export interface InputIssue {
2
2
  path: string;
3
3
  message: string;
4
4
  }
5
+ export declare function normalizeArgAliases(inputSchema: Record<string, unknown>, args: Record<string, unknown>): Record<string, unknown>;
5
6
  export declare function validateToolInput(inputSchema: Record<string, unknown>, args: Record<string, unknown>): InputIssue[];
6
7
  export declare function inputValidationError(toolName: string, issues: InputIssue[]): string;
@@ -8,6 +8,10 @@ export declare const schema: {
8
8
  type: string;
9
9
  description: string;
10
10
  };
11
+ projectPath: {
12
+ type: string;
13
+ description: string;
14
+ };
11
15
  browsers: {
12
16
  type: string;
13
17
  items: {
@@ -17,10 +21,11 @@ export declare const schema: {
17
21
  description: string;
18
22
  };
19
23
  };
20
- required: string[];
24
+ required: never[];
21
25
  };
22
26
  };
23
27
  export declare function handler(args: {
24
- manifestPath: string;
28
+ manifestPath?: string;
29
+ projectPath?: string;
25
30
  browsers?: string[];
26
31
  }): Promise<string>;
@@ -10,7 +10,6 @@ export declare const schema: {
10
10
  };
11
11
  browser: {
12
12
  type: string;
13
- default: string;
14
13
  description: string;
15
14
  };
16
15
  all: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "4.7.0",
4
+ "version": "4.8.0",
5
5
  "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 31 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": "MIT",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "4.7.0",
10
+ "version": "4.8.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@extension.dev/mcp",
16
- "version": "4.7.0",
16
+ "version": "4.8.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"