@extension.dev/mcp 4.8.0 → 4.9.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 +38 -0
- package/dist/module.js +384 -54
- package/dist/src/lib/session-browser.d.ts +4 -0
- package/dist/src/tools/manifest-validate.d.ts +5 -0
- package/dist/src/tools/open.d.ts +6 -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.9.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.9.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "boss@cezaraugusto.net",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.0
|
|
4
|
+
|
|
5
|
+
A second pass from the persona swarm, closing the gaps 4.8.0 left and the top
|
|
6
|
+
new blockers it surfaced.
|
|
7
|
+
|
|
8
|
+
- **Honest `extension_manifest_validate`.** It now scans the project source for
|
|
9
|
+
permission-gated `chrome.*`/`browser.*` calls and flags any the manifest does
|
|
10
|
+
not declare — an API used without its permission is `undefined` at runtime and
|
|
11
|
+
crashes the context, the exact case where validate used to report `valid:true`.
|
|
12
|
+
The headline is now honest (`valid:false` + `buildBlocking:true` on any error),
|
|
13
|
+
and it accepts singular `browser` as an alias for `browsers`.
|
|
14
|
+
- **`extension_open` can navigate a tab.** Pass a `url` (Chromium, via CDP) to
|
|
15
|
+
drive a content-script test page, a `webNavigation` target, or the popup as a
|
|
16
|
+
page (`chrome-extension://<id>/popup.html`) — the loop the surface-only open
|
|
17
|
+
could not do. `target` is accepted as an alias for `surface`.
|
|
18
|
+
- **`extension_stop` actually reaps the session.** It now terminates the dev CLI
|
|
19
|
+
and both browser families (gecko profile + chromium `--load-extension`, under
|
|
20
|
+
the project's dist) and refuses to report `stopped:true` while any survive.
|
|
21
|
+
- **`extension_wait` won't lie about a dead session.** A `ready.json` whose pid
|
|
22
|
+
is dead now returns `status:"stale"` instead of `ready`, so you don't walk into
|
|
23
|
+
a reload/eval that fails with a misleading control-channel error.
|
|
24
|
+
- **Dropped-channel errors name the real cause.** A `1006` / "no control channel"
|
|
25
|
+
now detects an exited dev server (stale ready.json + dead pid) and says so,
|
|
26
|
+
instead of asking "is the session started with allowControl?" when it was.
|
|
27
|
+
- **`extension_doctor`** surfaces recent error-level logs as a `runtime-errors`
|
|
28
|
+
check (so a background throwing on every event isn't `healthy:true`), keeps the
|
|
29
|
+
project-local engine version in project mode, and flags when that engine
|
|
30
|
+
differs from a pinned `EXTENSION_MCP_CLI_VERSION`.
|
|
31
|
+
- **`extension_build`** lists declared entrypoints in its success output, so a
|
|
32
|
+
content script no longer reads as "didn't build".
|
|
33
|
+
- **`extension_create`** forces non-interactive git (`GIT_TERMINAL_PROMPT=0`) so a
|
|
34
|
+
credential prompt can't hang the template download, retries once on a transient
|
|
35
|
+
network/timeout failure (cleaning the partial dir first), reports a download
|
|
36
|
+
failure as such instead of "choose a valid template name", and warns when the
|
|
37
|
+
scaffold's `extension@latest` pin will win over your pinned CLI.
|
|
38
|
+
- Eval/inspect error guidance now speaks MCP JSON args (`context`, `tab`, `url`)
|
|
39
|
+
instead of CLI flags.
|
|
40
|
+
|
|
3
41
|
## 4.8.0
|
|
4
42
|
|
|
5
43
|
Dev-session ergonomics hardened from a 30-persona agent walk of the toolchain.
|
package/dist/module.js
CHANGED
|
@@ -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.8.0","El":{"OP":"^4.0.11"}}');
|
|
203
203
|
function detectPackageManager(projectPath) {
|
|
204
204
|
const byLockfile = [
|
|
205
205
|
[
|
|
@@ -259,14 +259,27 @@ const create_schema = {
|
|
|
259
259
|
async function create_handler(args) {
|
|
260
260
|
const start = Date.now();
|
|
261
261
|
const projectInput = args.parentDir ? node_path.resolve(args.parentDir, args.projectName) : args.projectName;
|
|
262
|
+
process.env.GIT_TERMINAL_PROMPT = "0";
|
|
263
|
+
if (void 0 === process.env.GIT_ASKPASS) process.env.GIT_ASKPASS = "";
|
|
262
264
|
const logLines = [];
|
|
263
265
|
const capture = (stream)=>(...parts)=>{
|
|
264
266
|
const line = parts.map((p)=>"string" == typeof p ? p : String(p)).join(" ").trim();
|
|
265
267
|
if (line) logLines.push("error" === stream ? `[error] ${line}` : line);
|
|
266
268
|
};
|
|
267
269
|
const logTail = (max = 20)=>logLines.slice(-max);
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
+
const looksTransient = ()=>{
|
|
271
|
+
const blob = logLines.join("\n").toLowerCase();
|
|
272
|
+
return /timed out|timeout|etimedout|econnreset|rate limit|\b429\b|network|could not resolve host|terminal prompts disabled|authentication failed|early eof|rpc failed|remote end hung up/.test(blob);
|
|
273
|
+
};
|
|
274
|
+
const cleanPartial = ()=>{
|
|
275
|
+
try {
|
|
276
|
+
if (args.parentDir && node_fs.existsSync(projectInput)) node_fs.rmSync(projectInput, {
|
|
277
|
+
recursive: true,
|
|
278
|
+
force: true
|
|
279
|
+
});
|
|
280
|
+
} catch {}
|
|
281
|
+
};
|
|
282
|
+
const attempt = ()=>extensionCreate(projectInput, {
|
|
270
283
|
template: args.template ?? "typescript",
|
|
271
284
|
install: args.install ?? true,
|
|
272
285
|
logger: {
|
|
@@ -274,34 +287,53 @@ async function create_handler(args) {
|
|
|
274
287
|
error: capture("error")
|
|
275
288
|
}
|
|
276
289
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
template: result.template,
|
|
283
|
-
depsInstalled: result.depsInstalled,
|
|
284
|
-
packageManager: result.depsInstalled ? packageManager : null,
|
|
285
|
-
duration: Date.now() - start,
|
|
286
|
-
nextSteps: result.depsInstalled ? [
|
|
287
|
-
`cd ${result.projectPath}`,
|
|
288
|
-
runDev
|
|
289
|
-
] : [
|
|
290
|
-
`cd ${result.projectPath}`,
|
|
291
|
-
"npm install",
|
|
292
|
-
"npm run dev"
|
|
293
|
-
],
|
|
294
|
-
...result.depsInstalled ? {} : {
|
|
295
|
-
warnings: logTail()
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
} catch (err) {
|
|
299
|
-
return JSON.stringify({
|
|
300
|
-
error: err instanceof Error ? err.message : String(err),
|
|
290
|
+
const failure = (err, transient)=>JSON.stringify({
|
|
291
|
+
error: transient ? "Template download failed (network/timeout/rate-limit) — this is not a bad template name. Retry, or check connectivity/GitHub rate limits." : err instanceof Error ? err.message : String(err),
|
|
292
|
+
...transient ? {
|
|
293
|
+
cause: err instanceof Error ? err.message : String(err)
|
|
294
|
+
} : {},
|
|
301
295
|
duration: Date.now() - start,
|
|
302
296
|
log: logTail()
|
|
303
297
|
});
|
|
298
|
+
let result;
|
|
299
|
+
try {
|
|
300
|
+
result = await attempt();
|
|
301
|
+
} catch (err1) {
|
|
302
|
+
if (!looksTransient()) return failure(err1, false);
|
|
303
|
+
logLines.push("[retry] transient template-download failure; retrying once");
|
|
304
|
+
cleanPartial();
|
|
305
|
+
try {
|
|
306
|
+
result = await attempt();
|
|
307
|
+
} catch (err2) {
|
|
308
|
+
return failure(err2, looksTransient());
|
|
309
|
+
}
|
|
304
310
|
}
|
|
311
|
+
const packageManager = result.depsInstalled ? detectPackageManager(result.projectPath) : "npm";
|
|
312
|
+
const runDev = `${packageManager} run dev`;
|
|
313
|
+
const pin = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
314
|
+
const engineWarning = pin && "latest" !== pin ? `The scaffold pins "extension": "latest"; the project-local engine wins over EXTENSION_MCP_CLI_VERSION=${pin}. Run \`(cd ${result.projectPath} && npm i -D extension@${pin})\` to match the pinned engine.` : void 0;
|
|
315
|
+
return JSON.stringify({
|
|
316
|
+
projectPath: result.projectPath,
|
|
317
|
+
projectName: result.projectName,
|
|
318
|
+
template: result.template,
|
|
319
|
+
depsInstalled: result.depsInstalled,
|
|
320
|
+
packageManager: result.depsInstalled ? packageManager : null,
|
|
321
|
+
duration: Date.now() - start,
|
|
322
|
+
nextSteps: result.depsInstalled ? [
|
|
323
|
+
`cd ${result.projectPath}`,
|
|
324
|
+
runDev
|
|
325
|
+
] : [
|
|
326
|
+
`cd ${result.projectPath}`,
|
|
327
|
+
"npm install",
|
|
328
|
+
"npm run dev"
|
|
329
|
+
],
|
|
330
|
+
...engineWarning ? {
|
|
331
|
+
engineWarning
|
|
332
|
+
} : {},
|
|
333
|
+
...result.depsInstalled ? {} : {
|
|
334
|
+
warnings: logTail()
|
|
335
|
+
}
|
|
336
|
+
});
|
|
305
337
|
}
|
|
306
338
|
const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
|
|
307
339
|
const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
|
|
@@ -535,6 +567,34 @@ function spawnExtensionCli(args, options) {
|
|
|
535
567
|
child.unref();
|
|
536
568
|
return child;
|
|
537
569
|
}
|
|
570
|
+
function builtEntrypoints(distDir) {
|
|
571
|
+
let manifest;
|
|
572
|
+
try {
|
|
573
|
+
manifest = JSON.parse(node_fs.readFileSync(node_path.join(distDir, "manifest.json"), "utf8"));
|
|
574
|
+
} catch {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
const out = [];
|
|
578
|
+
const add = (role, ref)=>{
|
|
579
|
+
if ("string" != typeof ref) return;
|
|
580
|
+
out.push({
|
|
581
|
+
role,
|
|
582
|
+
path: ref,
|
|
583
|
+
present: node_fs.existsSync(node_path.join(distDir, ref.replace(/^\.?\//, "")))
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
const bg = manifest.background;
|
|
587
|
+
if (bg?.service_worker) add("background.service_worker", bg.service_worker);
|
|
588
|
+
if (Array.isArray(bg?.scripts)) bg.scripts.forEach((s)=>add("background.scripts", s));
|
|
589
|
+
const action = manifest.action || manifest.browser_action;
|
|
590
|
+
if (action?.default_popup) add("action.default_popup", action.default_popup);
|
|
591
|
+
const cs = manifest.content_scripts;
|
|
592
|
+
if (Array.isArray(cs)) cs.forEach((c, i)=>{
|
|
593
|
+
if (Array.isArray(c.js)) c.js.forEach((j)=>add(`content_scripts[${i}].js`, j));
|
|
594
|
+
if (Array.isArray(c.css)) c.css.forEach((s)=>add(`content_scripts[${i}].css`, s));
|
|
595
|
+
});
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
538
598
|
const build_schema = {
|
|
539
599
|
name: "extension_build",
|
|
540
600
|
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
@@ -632,6 +692,7 @@ async function build_handler(args) {
|
|
|
632
692
|
if (0 === code) {
|
|
633
693
|
const size = out.match(/Size:\s*([\d.]+\s*[kKmMgG]?B)/)?.[1];
|
|
634
694
|
const status = out.match(/Build Status:\s*(\w+)/)?.[1];
|
|
695
|
+
const entrypoints = builtEntrypoints(node_path.resolve(args.projectPath, "dist", browser));
|
|
635
696
|
return JSON.stringify({
|
|
636
697
|
success: true,
|
|
637
698
|
browser,
|
|
@@ -641,6 +702,9 @@ async function build_handler(args) {
|
|
|
641
702
|
...status ? {
|
|
642
703
|
status
|
|
643
704
|
} : {},
|
|
705
|
+
...entrypoints.length ? {
|
|
706
|
+
entrypoints
|
|
707
|
+
} : {},
|
|
644
708
|
zip: args.zip ?? false,
|
|
645
709
|
duration,
|
|
646
710
|
output: lastLines(out, 12)
|
|
@@ -1050,6 +1114,13 @@ function knownSessionBrowsers(projectPath) {
|
|
|
1050
1114
|
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
1051
1115
|
return Array.from(new Set(browsers));
|
|
1052
1116
|
}
|
|
1117
|
+
function deadReadySession(projectPath) {
|
|
1118
|
+
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
1119
|
+
browser: sighting.browser,
|
|
1120
|
+
pid: sighting.pid
|
|
1121
|
+
};
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1053
1124
|
function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
|
|
1054
1125
|
if (explicit) return {
|
|
1055
1126
|
browser: explicit,
|
|
@@ -1094,12 +1165,11 @@ const stop_schema = {
|
|
|
1094
1165
|
required: []
|
|
1095
1166
|
}
|
|
1096
1167
|
};
|
|
1097
|
-
function
|
|
1098
|
-
const marker = node_path.resolve(projectPath, "dist", `extension-profile-${browser}`);
|
|
1168
|
+
function pgrepPids(pattern) {
|
|
1099
1169
|
try {
|
|
1100
1170
|
const out = execFileSync("pgrep", [
|
|
1101
1171
|
"-f",
|
|
1102
|
-
|
|
1172
|
+
pattern
|
|
1103
1173
|
], {
|
|
1104
1174
|
encoding: "utf8"
|
|
1105
1175
|
});
|
|
@@ -1108,8 +1178,19 @@ function profileProcessPids(projectPath, browser) {
|
|
|
1108
1178
|
return [];
|
|
1109
1179
|
}
|
|
1110
1180
|
}
|
|
1111
|
-
function
|
|
1112
|
-
const
|
|
1181
|
+
function sessionProcessPids(projectPath) {
|
|
1182
|
+
const resolved = node_path.resolve(projectPath);
|
|
1183
|
+
const pids = new Set();
|
|
1184
|
+
for (const marker of [
|
|
1185
|
+
`extension dev ${resolved}`,
|
|
1186
|
+
node_path.join(resolved, "dist")
|
|
1187
|
+
])for (const pid of pgrepPids(marker))pids.add(pid);
|
|
1188
|
+
return [
|
|
1189
|
+
...pids
|
|
1190
|
+
];
|
|
1191
|
+
}
|
|
1192
|
+
function reapSessionProcesses(projectPath) {
|
|
1193
|
+
const pids = sessionProcessPids(projectPath);
|
|
1113
1194
|
for (const pid of pids)try {
|
|
1114
1195
|
process.kill(pid, "SIGKILL");
|
|
1115
1196
|
} catch {}
|
|
@@ -1152,7 +1233,7 @@ async function stopOne(projectPath, browser) {
|
|
|
1152
1233
|
const session = getSession(projectPath, browser);
|
|
1153
1234
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
1154
1235
|
if (null == pid) {
|
|
1155
|
-
const reaped =
|
|
1236
|
+
const reaped = reapSessionProcesses(projectPath);
|
|
1156
1237
|
return {
|
|
1157
1238
|
projectPath,
|
|
1158
1239
|
browser,
|
|
@@ -1172,14 +1253,14 @@ async function stopOne(projectPath, browser) {
|
|
|
1172
1253
|
}
|
|
1173
1254
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
1174
1255
|
} else detail = "Process was already gone; cleaned up session records.";
|
|
1175
|
-
const reaped =
|
|
1256
|
+
const reaped = reapSessionProcesses(projectPath);
|
|
1176
1257
|
removeSession(projectPath, browser);
|
|
1177
1258
|
try {
|
|
1178
1259
|
node_fs.rmSync(readyJsonPath(projectPath, browser), {
|
|
1179
1260
|
force: true
|
|
1180
1261
|
});
|
|
1181
1262
|
} catch {}
|
|
1182
|
-
const survivors =
|
|
1263
|
+
const survivors = sessionProcessPids(projectPath);
|
|
1183
1264
|
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
1184
1265
|
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
1185
1266
|
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
@@ -1413,6 +1494,10 @@ const manifest_validate_schema = {
|
|
|
1413
1494
|
"firefox"
|
|
1414
1495
|
],
|
|
1415
1496
|
description: "Browsers to validate against"
|
|
1497
|
+
},
|
|
1498
|
+
browser: {
|
|
1499
|
+
type: "string",
|
|
1500
|
+
description: "Single browser to validate against; alias for browsers:[browser] to match the other tools."
|
|
1416
1501
|
}
|
|
1417
1502
|
},
|
|
1418
1503
|
required: []
|
|
@@ -1473,7 +1558,90 @@ function findManifest(projectPath) {
|
|
|
1473
1558
|
}
|
|
1474
1559
|
return null;
|
|
1475
1560
|
}
|
|
1561
|
+
const API_PERMISSION = {
|
|
1562
|
+
storage: "storage",
|
|
1563
|
+
webNavigation: "webNavigation",
|
|
1564
|
+
history: "history",
|
|
1565
|
+
cookies: "cookies",
|
|
1566
|
+
bookmarks: "bookmarks",
|
|
1567
|
+
alarms: "alarms",
|
|
1568
|
+
contextMenus: "contextMenus",
|
|
1569
|
+
notifications: "notifications",
|
|
1570
|
+
downloads: "downloads",
|
|
1571
|
+
webRequest: "webRequest",
|
|
1572
|
+
tabGroups: "tabGroups",
|
|
1573
|
+
topSites: "topSites",
|
|
1574
|
+
idle: "idle",
|
|
1575
|
+
management: "management",
|
|
1576
|
+
scripting: "scripting",
|
|
1577
|
+
declarativeNetRequest: "declarativeNetRequest",
|
|
1578
|
+
sessions: "sessions",
|
|
1579
|
+
proxy: "proxy",
|
|
1580
|
+
tts: "tts",
|
|
1581
|
+
pageCapture: "pageCapture",
|
|
1582
|
+
desktopCapture: "desktopCapture",
|
|
1583
|
+
debugger: "debugger",
|
|
1584
|
+
geolocation: "geolocation"
|
|
1585
|
+
};
|
|
1586
|
+
const HARD_APIS = new Set([
|
|
1587
|
+
"history",
|
|
1588
|
+
"cookies",
|
|
1589
|
+
"bookmarks",
|
|
1590
|
+
"webNavigation",
|
|
1591
|
+
"downloads",
|
|
1592
|
+
"webRequest",
|
|
1593
|
+
"topSites",
|
|
1594
|
+
"management",
|
|
1595
|
+
"tabGroups",
|
|
1596
|
+
"sessions",
|
|
1597
|
+
"proxy",
|
|
1598
|
+
"debugger",
|
|
1599
|
+
"pageCapture",
|
|
1600
|
+
"desktopCapture"
|
|
1601
|
+
]);
|
|
1602
|
+
function scanApiUsage(roots) {
|
|
1603
|
+
const used = new Set();
|
|
1604
|
+
let filesRead = 0;
|
|
1605
|
+
const walk = (dir, depth)=>{
|
|
1606
|
+
if (depth > 6 || filesRead > 300) return;
|
|
1607
|
+
let entries;
|
|
1608
|
+
try {
|
|
1609
|
+
entries = node_fs.readdirSync(dir, {
|
|
1610
|
+
withFileTypes: true
|
|
1611
|
+
});
|
|
1612
|
+
} catch {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
for (const e of entries){
|
|
1616
|
+
if ("node_modules" === e.name || "dist" === e.name || e.name.startsWith(".")) continue;
|
|
1617
|
+
const full = node_path.join(dir, e.name);
|
|
1618
|
+
if (e.isDirectory()) {
|
|
1619
|
+
walk(full, depth + 1);
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (!/\.(js|mjs|cjs|ts|tsx|jsx|svelte|vue)$/.test(e.name)) continue;
|
|
1623
|
+
if (filesRead++ > 300) return;
|
|
1624
|
+
let src;
|
|
1625
|
+
try {
|
|
1626
|
+
src = node_fs.readFileSync(full, "utf8");
|
|
1627
|
+
} catch {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const re = /\b(?:chrome|browser)\.(\w+)/g;
|
|
1631
|
+
let m;
|
|
1632
|
+
while(m = re.exec(src))if (API_PERMISSION[m[1]]) used.add(m[1]);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
for (const root of new Set(roots))walk(root, 0);
|
|
1636
|
+
return used;
|
|
1637
|
+
}
|
|
1476
1638
|
async function manifest_validate_handler(args) {
|
|
1639
|
+
if (!args.browsers && "string" == typeof args.browser) args = {
|
|
1640
|
+
...args,
|
|
1641
|
+
browsers: [
|
|
1642
|
+
args.browser
|
|
1643
|
+
]
|
|
1644
|
+
};
|
|
1477
1645
|
const browsers = args.browsers ?? [
|
|
1478
1646
|
"chrome",
|
|
1479
1647
|
"firefox"
|
|
@@ -1522,6 +1690,17 @@ async function manifest_validate_handler(args) {
|
|
|
1522
1690
|
] : []
|
|
1523
1691
|
];
|
|
1524
1692
|
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.`);
|
|
1693
|
+
const declaredPermSet = new Set([
|
|
1694
|
+
...chromiumManifest.permissions ?? [],
|
|
1695
|
+
...chromiumManifest.optional_permissions ?? []
|
|
1696
|
+
].filter((p)=>"string" == typeof p));
|
|
1697
|
+
for (const api of scanApiUsage(roots)){
|
|
1698
|
+
const perm = API_PERMISSION[api];
|
|
1699
|
+
if (declaredPermSet.has(perm)) continue;
|
|
1700
|
+
const base = `Code calls chrome.${api} but "${perm}" is not in permissions`;
|
|
1701
|
+
if (HARD_APIS.has(api)) result.errors.push(`${base} — chrome.${api} is undefined without it and will crash the context at runtime.`);
|
|
1702
|
+
else result.warnings.push(`${base}; it may be undefined at runtime — add "${perm}" if you use it.`);
|
|
1703
|
+
}
|
|
1525
1704
|
if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
1526
1705
|
const declaredPerms = [
|
|
1527
1706
|
...chromiumManifest.permissions ?? [],
|
|
@@ -1580,8 +1759,11 @@ async function manifest_validate_handler(args) {
|
|
|
1580
1759
|
surfaces: t.surfaces
|
|
1581
1760
|
}));
|
|
1582
1761
|
} catch {}
|
|
1583
|
-
|
|
1584
|
-
return JSON.stringify(
|
|
1762
|
+
result.valid = 0 === result.errors.length && Object.values(result.browserSupport).every((b)=>b.supported);
|
|
1763
|
+
return JSON.stringify({
|
|
1764
|
+
...result,
|
|
1765
|
+
buildBlocking: result.errors.length > 0
|
|
1766
|
+
});
|
|
1585
1767
|
}
|
|
1586
1768
|
const inspect_schema = {
|
|
1587
1769
|
name: "extension_inspect",
|
|
@@ -2787,10 +2969,13 @@ async function logs_handler(args) {
|
|
|
2787
2969
|
return readFromFile(args, browser, limit);
|
|
2788
2970
|
}
|
|
2789
2971
|
function toMcpSpeak(text) {
|
|
2790
|
-
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(/--browser[= ]([\w-]+)/g, 'browser: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev");
|
|
2972
|
+
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(/--context[= ]([\w-]+)/g, 'context: "$1"').replace(/--tab[= ](\d+)/g, "tab: $1").replace(/--url[= ](\S+)/g, 'url: "$1"').replace(/--browser[= ]([\w-]+)/g, 'browser: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev");
|
|
2791
2973
|
}
|
|
2792
2974
|
function withSessionContext(message, projectPath) {
|
|
2793
|
-
|
|
2975
|
+
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
2976
|
+
if (!isControlError) return message;
|
|
2977
|
+
const dead = deadReadySession(projectPath);
|
|
2978
|
+
if (dead) return `${message}\nLikely cause: the dev server has exited — ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
|
|
2794
2979
|
const running = knownSessionBrowsers(projectPath);
|
|
2795
2980
|
if (0 === running.length) return message;
|
|
2796
2981
|
return `${message} Active session browser(s) for this project: ${running.join(", ")} — pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
|
|
@@ -3031,6 +3216,64 @@ async function reload_handler(args) {
|
|
|
3031
3216
|
})
|
|
3032
3217
|
], args.projectPath, args.timeout);
|
|
3033
3218
|
}
|
|
3219
|
+
async function navigateToUrl(projectPath, browser, url) {
|
|
3220
|
+
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
3221
|
+
ok: false,
|
|
3222
|
+
error: {
|
|
3223
|
+
name: "Unsupported",
|
|
3224
|
+
message: `URL navigation drives a tab over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. On Firefox, drive the page via extension_eval (context: "page"/"content") or read extension_logs.`
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3228
|
+
if (!resolved) return JSON.stringify({
|
|
3229
|
+
ok: false,
|
|
3230
|
+
error: {
|
|
3231
|
+
name: "NoSession",
|
|
3232
|
+
message: `No active dev session / CDP port for ${browser}. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
const cdp = new CDPClient();
|
|
3236
|
+
try {
|
|
3237
|
+
const targets = await CDPClient.discoverTargets(resolved.port);
|
|
3238
|
+
const pageTargets = targets.filter((t)=>"page" === t.type && !String(t.url || "").startsWith("devtools://"));
|
|
3239
|
+
if (0 === pageTargets.length) return JSON.stringify({
|
|
3240
|
+
ok: false,
|
|
3241
|
+
error: {
|
|
3242
|
+
name: "NoTab",
|
|
3243
|
+
message: "The dev browser has no open page tab to navigate. Trigger one (e.g. extension_open surface, or open the extension) first."
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
const target = pageTargets[0];
|
|
3247
|
+
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(resolved.port);
|
|
3248
|
+
await cdp.connect(browserWsUrl);
|
|
3249
|
+
const sessionId = await cdp.attachToTarget(String(target.id));
|
|
3250
|
+
await cdp.navigate(sessionId, url);
|
|
3251
|
+
await new Promise((r)=>setTimeout(r, 1200));
|
|
3252
|
+
const meta = await cdp.getPageMeta(sessionId).catch(()=>({}));
|
|
3253
|
+
return JSON.stringify({
|
|
3254
|
+
ok: true,
|
|
3255
|
+
navigated: url,
|
|
3256
|
+
tab: {
|
|
3257
|
+
id: target.id,
|
|
3258
|
+
title: meta.title,
|
|
3259
|
+
url: meta.url || url
|
|
3260
|
+
},
|
|
3261
|
+
hint: "Inspect it with extension_dom_inspect or extension_source_inspect (context: 'page')."
|
|
3262
|
+
});
|
|
3263
|
+
} catch (e) {
|
|
3264
|
+
return JSON.stringify({
|
|
3265
|
+
ok: false,
|
|
3266
|
+
error: {
|
|
3267
|
+
name: "NavigateError",
|
|
3268
|
+
message: e instanceof Error ? e.message : String(e)
|
|
3269
|
+
}
|
|
3270
|
+
});
|
|
3271
|
+
} finally{
|
|
3272
|
+
try {
|
|
3273
|
+
cdp.disconnect();
|
|
3274
|
+
} catch {}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3034
3277
|
const open_schema = {
|
|
3035
3278
|
name: "extension_open",
|
|
3036
3279
|
description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces. 'action' triggers the toolbar action: opens the action's popup, or (no popup) replays chrome.action.onClicked. 'command' replays a chrome.commands.onCommand keyboard shortcut (pass `name`). NOTE: action/command replay invokes your listener WITHOUT a user gesture, so the gesture-derived activeTab grant does not apply (the result includes gesture:false and a warning when activeTab is declared). Requires the dev session to be started with allowControl: true (extension_dev). Wraps `extension open`.",
|
|
@@ -3056,6 +3299,10 @@ const open_schema = {
|
|
|
3056
3299
|
type: "string",
|
|
3057
3300
|
description: "For surface 'command': the chrome.commands name to trigger."
|
|
3058
3301
|
},
|
|
3302
|
+
url: {
|
|
3303
|
+
type: "string",
|
|
3304
|
+
description: "Navigate a real tab to this URL (Chromium only, via CDP) instead of opening a surface. Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
|
|
3305
|
+
},
|
|
3059
3306
|
browser: {
|
|
3060
3307
|
type: "string",
|
|
3061
3308
|
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
@@ -3066,13 +3313,20 @@ const open_schema = {
|
|
|
3066
3313
|
}
|
|
3067
3314
|
},
|
|
3068
3315
|
required: [
|
|
3069
|
-
"projectPath"
|
|
3070
|
-
"surface"
|
|
3316
|
+
"projectPath"
|
|
3071
3317
|
]
|
|
3072
3318
|
}
|
|
3073
3319
|
};
|
|
3074
3320
|
async function open_handler(args) {
|
|
3075
3321
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
3322
|
+
if (args.url) return navigateToUrl(args.projectPath, browser, args.url);
|
|
3323
|
+
if (!args.surface) return JSON.stringify({
|
|
3324
|
+
ok: false,
|
|
3325
|
+
error: {
|
|
3326
|
+
name: "BadRequest",
|
|
3327
|
+
message: "Pass `surface` (popup/options/sidebar/action/command) to open a surface, or `url` to navigate a tab."
|
|
3328
|
+
}
|
|
3329
|
+
});
|
|
3076
3330
|
const cli = [
|
|
3077
3331
|
"open",
|
|
3078
3332
|
args.surface,
|
|
@@ -3679,6 +3933,14 @@ async function deploy_handler(args) {
|
|
|
3679
3933
|
});
|
|
3680
3934
|
}
|
|
3681
3935
|
const SAFE_CEILING_MS = 50000;
|
|
3936
|
+
function wait_isAlive(pid) {
|
|
3937
|
+
try {
|
|
3938
|
+
process.kill(pid, 0);
|
|
3939
|
+
return true;
|
|
3940
|
+
} catch {
|
|
3941
|
+
return false;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3682
3944
|
const wait_schema = {
|
|
3683
3945
|
name: "extension_wait",
|
|
3684
3946
|
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status. A single call is bounded to ~50s to stay under the MCP client request timeout; if it returns status:'timeout', call it again to keep waiting (polling resumes on the same contract).",
|
|
@@ -3716,18 +3978,27 @@ async function wait_handler(args) {
|
|
|
3716
3978
|
try {
|
|
3717
3979
|
const raw = node_fs.readFileSync(readyPath, "utf8");
|
|
3718
3980
|
const contract = JSON.parse(raw);
|
|
3719
|
-
if ("ready" === contract.status)
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3981
|
+
if ("ready" === contract.status) {
|
|
3982
|
+
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return JSON.stringify({
|
|
3983
|
+
status: "stale",
|
|
3984
|
+
message: `ready.json reports ready but its dev-server pid ${contract.pid} is dead — the session exited. Restart with extension_dev; extension_doctor will confirm.`,
|
|
3985
|
+
browser: contract.browser,
|
|
3986
|
+
pid: contract.pid,
|
|
3987
|
+
waitDuration: Date.now() - start
|
|
3988
|
+
});
|
|
3989
|
+
return JSON.stringify({
|
|
3990
|
+
status: "ready",
|
|
3991
|
+
command: contract.command,
|
|
3992
|
+
browser: contract.browser,
|
|
3993
|
+
port: contract.port,
|
|
3994
|
+
pid: contract.pid,
|
|
3995
|
+
distPath: contract.distPath,
|
|
3996
|
+
manifestPath: contract.manifestPath,
|
|
3997
|
+
compiledAt: contract.compiledAt,
|
|
3998
|
+
startedAt: contract.startedAt,
|
|
3999
|
+
waitDuration: Date.now() - start
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
3731
4002
|
if ("error" === contract.status) return JSON.stringify({
|
|
3732
4003
|
status: "error",
|
|
3733
4004
|
message: contract.message,
|
|
@@ -4925,6 +5196,37 @@ async function environmentPreflight() {
|
|
|
4925
5196
|
hint: "Pass projectPath to diagnose a live dev session end-to-end."
|
|
4926
5197
|
});
|
|
4927
5198
|
}
|
|
5199
|
+
function recentErrorLogs(projectPath, browser, max = 5) {
|
|
5200
|
+
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
5201
|
+
let lines;
|
|
5202
|
+
try {
|
|
5203
|
+
lines = node_fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
5204
|
+
} catch {
|
|
5205
|
+
return [];
|
|
5206
|
+
}
|
|
5207
|
+
const errs = [];
|
|
5208
|
+
for (const line of lines){
|
|
5209
|
+
let ev;
|
|
5210
|
+
try {
|
|
5211
|
+
ev = JSON.parse(line);
|
|
5212
|
+
} catch {
|
|
5213
|
+
continue;
|
|
5214
|
+
}
|
|
5215
|
+
if (ev && "error" === ev.level) {
|
|
5216
|
+
const msg = Array.isArray(ev.args) ? ev.args.join(" ") : ev.message || ev.text || "";
|
|
5217
|
+
if (msg) errs.push(String(msg).slice(0, 300));
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
return errs.slice(-max);
|
|
5221
|
+
}
|
|
5222
|
+
function projectEngineVersion(projectPath) {
|
|
5223
|
+
try {
|
|
5224
|
+
const p = node_path.resolve(projectPath, "node_modules", "extension", "package.json");
|
|
5225
|
+
return JSON.parse(node_fs.readFileSync(p, "utf8")).version || null;
|
|
5226
|
+
} catch {
|
|
5227
|
+
return null;
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
4928
5230
|
async function doctor_handler(args) {
|
|
4929
5231
|
if (!args.projectPath) return environmentPreflight();
|
|
4930
5232
|
const projectPath = args.projectPath;
|
|
@@ -4958,9 +5260,36 @@ async function doctor_handler(args) {
|
|
|
4958
5260
|
detail: toMcpSpeak(detail),
|
|
4959
5261
|
remediation: "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
4960
5262
|
});
|
|
5263
|
+
} else {
|
|
5264
|
+
const errs = recentErrorLogs(projectPath, browser);
|
|
5265
|
+
if (errs.length) {
|
|
5266
|
+
healthy = false;
|
|
5267
|
+
checks.push({
|
|
5268
|
+
check: "runtime-errors",
|
|
5269
|
+
status: "fail",
|
|
5270
|
+
detail: `Recent error-level logs: ${errs.join(" | ")}`,
|
|
5271
|
+
remediation: "The extension is throwing at runtime. Inspect with extension_logs; a missing permission for a called chrome.* API is a common cause (extension_manifest_validate now checks this)."
|
|
5272
|
+
});
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
const engineVersion = projectEngineVersion(projectPath);
|
|
5276
|
+
if (engineVersion) {
|
|
5277
|
+
const pin = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
5278
|
+
const mismatch = "" !== pin && "latest" !== pin && !engineVersion.includes(pin);
|
|
5279
|
+
checks.push({
|
|
5280
|
+
check: "project-engine",
|
|
5281
|
+
status: mismatch ? "warn" : "pass",
|
|
5282
|
+
detail: `project-local extension@${engineVersion}${mismatch ? ` — but EXTENSION_MCP_CLI_VERSION=${pin}; the dev loop uses the project bin, not the pin` : ""}`,
|
|
5283
|
+
...mismatch ? {
|
|
5284
|
+
remediation: `Run \`(cd ${projectPath} && npm i -D extension@${pin})\` to match the pinned engine.`
|
|
5285
|
+
} : {}
|
|
5286
|
+
});
|
|
4961
5287
|
}
|
|
4962
5288
|
return JSON.stringify({
|
|
4963
5289
|
browser,
|
|
5290
|
+
...engineVersion ? {
|
|
5291
|
+
engineVersion
|
|
5292
|
+
} : {},
|
|
4964
5293
|
healthy,
|
|
4965
5294
|
checks
|
|
4966
5295
|
});
|
|
@@ -5035,7 +5364,8 @@ const ARG_ALIASES = {
|
|
|
5035
5364
|
"manifest"
|
|
5036
5365
|
],
|
|
5037
5366
|
surface: [
|
|
5038
|
-
"view"
|
|
5367
|
+
"view",
|
|
5368
|
+
"target"
|
|
5039
5369
|
]
|
|
5040
5370
|
};
|
|
5041
5371
|
function normalizeArgAliases(inputSchema, args) {
|
|
@@ -3,4 +3,8 @@ export interface ResolvedBrowser {
|
|
|
3
3
|
source: "explicit" | "session" | "contract" | "fallback";
|
|
4
4
|
}
|
|
5
5
|
export declare function knownSessionBrowsers(projectPath: string): string[];
|
|
6
|
+
export declare function deadReadySession(projectPath: string): {
|
|
7
|
+
browser: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
} | null;
|
|
6
10
|
export declare function resolveSessionBrowser(projectPath: string, explicit: string | undefined, fallback?: string): ResolvedBrowser;
|
|
@@ -20,6 +20,10 @@ export declare const schema: {
|
|
|
20
20
|
default: string[];
|
|
21
21
|
description: string;
|
|
22
22
|
};
|
|
23
|
+
browser: {
|
|
24
|
+
type: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
23
27
|
};
|
|
24
28
|
required: never[];
|
|
25
29
|
};
|
|
@@ -27,5 +31,6 @@ export declare const schema: {
|
|
|
27
31
|
export declare function handler(args: {
|
|
28
32
|
manifestPath?: string;
|
|
29
33
|
projectPath?: string;
|
|
34
|
+
browser?: string;
|
|
30
35
|
browsers?: string[];
|
|
31
36
|
}): Promise<string>;
|
package/dist/src/tools/open.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare const schema: {
|
|
|
18
18
|
type: string;
|
|
19
19
|
description: string;
|
|
20
20
|
};
|
|
21
|
+
url: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
21
25
|
browser: {
|
|
22
26
|
type: string;
|
|
23
27
|
description: string;
|
|
@@ -31,6 +35,7 @@ export declare const schema: {
|
|
|
31
35
|
};
|
|
32
36
|
};
|
|
33
37
|
export declare function handler(args: ActArgs & {
|
|
34
|
-
surface
|
|
38
|
+
surface?: string;
|
|
35
39
|
name?: string;
|
|
40
|
+
url?: string;
|
|
36
41
|
}): Promise<string>;
|
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.9.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.9.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.9.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|