@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -0
- package/dist/module.js +309 -86
- package/dist/src/lib/validate-input.d.ts +1 -0
- package/dist/src/tools/manifest-validate.d.ts +7 -2
- package/dist/src/tools/stop.d.ts +0 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -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.
|
|
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.
|
|
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.
|
|
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: "
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1067
|
-
projectPath,
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
2161
|
-
hint:
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
24
|
+
required: never[];
|
|
21
25
|
};
|
|
22
26
|
};
|
|
23
27
|
export declare function handler(args: {
|
|
24
|
-
manifestPath
|
|
28
|
+
manifestPath?: string;
|
|
29
|
+
projectPath?: string;
|
|
25
30
|
browsers?: string[];
|
|
26
31
|
}): Promise<string>;
|
package/dist/src/tools/stop.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
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.
|
|
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.
|
|
16
|
+
"version": "4.8.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|