@extension.dev/mcp 5.5.1 → 5.5.2
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 +11 -0
- package/README.md +1 -1
- package/dist/module.js +381 -114
- package/dist/src/lib/bridge-tabs.d.ts +14 -0
- package/package.json +2 -2
- 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": "5.5.
|
|
13
|
+
"version": "5.5.2",
|
|
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": "5.5.
|
|
4
|
+
"version": "5.5.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "hello@extension.dev",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.5.2
|
|
4
|
+
|
|
5
|
+
Honest browser support wording ahead of the Safari lane landing.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The package description and README now say Safari is coming next
|
|
10
|
+
instead of listing it alongside the browsers that are store-ready
|
|
11
|
+
today. Chrome, Edge, Firefox, and every Chromium- or Gecko-based
|
|
12
|
+
browser remain fully supported; nothing changes functionally.
|
|
13
|
+
|
|
3
14
|
## 5.5.1
|
|
4
15
|
|
|
5
16
|
The store journey stops being write-only after submit, and the token
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Extensions fail silently: content scripts that never inject, panels that never o
|
|
|
26
26
|
These tools give agents eyes on the live browser, so they debug from evidence instead of guessing:
|
|
27
27
|
|
|
28
28
|
- **Scaffold** from the 60+ template catalog behind [templates.extension.dev](https://templates.extension.dev), or add a popup, sidebar, or content script to an existing project
|
|
29
|
-
- **Run** the dev server with HMR in Chrome, Edge, Firefox,
|
|
29
|
+
- **Run** the dev server with HMR in Chrome, Edge, Firefox, Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf, or any Chromium- or Gecko-based binary (Safari coming next), no build config
|
|
30
30
|
- **See** the live DOM, unified logs from every extension context, `chrome.storage` contents, and the loaded-extension list
|
|
31
31
|
- **Act**: evaluate code in any context, trigger the action button and commands, reload the extension, replay events
|
|
32
32
|
- **Ship**: validate the manifest cross-browser, build for production, publish a shareable preview, and promote builds to release channels headlessly
|
package/dist/module.js
CHANGED
|
@@ -219,7 +219,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
219
219
|
handler: ()=>whoami_handler,
|
|
220
220
|
schema: ()=>whoami_schema
|
|
221
221
|
});
|
|
222
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.5.
|
|
222
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.5.2","El":{"OP":"^4.0.13"}}');
|
|
223
223
|
function scaffoldEnginePin(projectPath) {
|
|
224
224
|
try {
|
|
225
225
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -3012,9 +3012,178 @@ function isCdpEndpoint(port) {
|
|
|
3012
3012
|
});
|
|
3013
3013
|
});
|
|
3014
3014
|
}
|
|
3015
|
+
function toMcpSpeak(text) {
|
|
3016
|
+
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
3017
|
+
}
|
|
3018
|
+
function withSessionContext(message, projectPath) {
|
|
3019
|
+
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
3020
|
+
if (!isControlError) return message;
|
|
3021
|
+
const dead = deadReadySession(projectPath);
|
|
3022
|
+
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.`;
|
|
3023
|
+
const running = knownSessionBrowsers(projectPath);
|
|
3024
|
+
if (0 === running.length) return message;
|
|
3025
|
+
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.`;
|
|
3026
|
+
}
|
|
3027
|
+
function translateFrame(frame, projectPath) {
|
|
3028
|
+
if (!frame || false !== frame.ok) return frame;
|
|
3029
|
+
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
|
|
3030
|
+
if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
|
|
3031
|
+
if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
|
|
3032
|
+
return frame;
|
|
3033
|
+
}
|
|
3034
|
+
async function runActVerb(args, projectPath, timeoutMs) {
|
|
3035
|
+
const { code, stdout, stderr } = await runExtensionCli([
|
|
3036
|
+
...args,
|
|
3037
|
+
"--output",
|
|
3038
|
+
"json"
|
|
3039
|
+
], {
|
|
3040
|
+
cwd: projectPath,
|
|
3041
|
+
timeoutMs
|
|
3042
|
+
});
|
|
3043
|
+
const out = stdout.trim();
|
|
3044
|
+
if (out) try {
|
|
3045
|
+
const frame = JSON.parse(out);
|
|
3046
|
+
if (frame && false === frame.ok) return JSON.stringify(translateFrame(frame, projectPath));
|
|
3047
|
+
return out;
|
|
3048
|
+
} catch {}
|
|
3049
|
+
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
3050
|
+
return JSON.stringify({
|
|
3051
|
+
ok: false,
|
|
3052
|
+
error: {
|
|
3053
|
+
name: "CliError",
|
|
3054
|
+
message: withSessionContext(toMcpSpeak(message), projectPath)
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
function commonFlags(args) {
|
|
3059
|
+
const flags = [];
|
|
3060
|
+
if (args.context) flags.push("--context", args.context);
|
|
3061
|
+
if (args.url) flags.push("--url", args.url);
|
|
3062
|
+
if (null != args.tab) flags.push("--tab", String(args.tab));
|
|
3063
|
+
if (args.browser) flags.push("--browser", args.browser);
|
|
3064
|
+
if (null != args.timeout) flags.push("--timeout", String(args.timeout));
|
|
3065
|
+
return flags;
|
|
3066
|
+
}
|
|
3067
|
+
async function listBridgeTabs(projectPath, browser, timeout) {
|
|
3068
|
+
const raw = await runActVerb([
|
|
3069
|
+
"inspect",
|
|
3070
|
+
projectPath,
|
|
3071
|
+
"--list-tabs",
|
|
3072
|
+
"--browser",
|
|
3073
|
+
browser,
|
|
3074
|
+
...null != timeout ? [
|
|
3075
|
+
"--timeout",
|
|
3076
|
+
String(timeout)
|
|
3077
|
+
] : []
|
|
3078
|
+
], projectPath, timeout);
|
|
3079
|
+
let parsed;
|
|
3080
|
+
try {
|
|
3081
|
+
parsed = JSON.parse(raw);
|
|
3082
|
+
} catch {
|
|
3083
|
+
return {
|
|
3084
|
+
error: raw
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
if (parsed?.ok === false) return {
|
|
3088
|
+
error: raw
|
|
3089
|
+
};
|
|
3090
|
+
const list = Array.isArray(parsed?.tabs) ? parsed.tabs : Array.isArray(parsed?.value) ? parsed.value : Array.isArray(parsed?.value?.tabs) ? parsed.value.tabs : null;
|
|
3091
|
+
if (!list) return {
|
|
3092
|
+
error: raw
|
|
3093
|
+
};
|
|
3094
|
+
return {
|
|
3095
|
+
tabs: list.map((t)=>({
|
|
3096
|
+
tabId: "number" == typeof t?.tabId ? t.tabId : "number" == typeof t?.id ? t.id : null,
|
|
3097
|
+
url: String(t?.url ?? ""),
|
|
3098
|
+
title: String(t?.title ?? "")
|
|
3099
|
+
}))
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
function matchTabsByUrl(tabs, needle) {
|
|
3103
|
+
const wanted = needle.toLowerCase();
|
|
3104
|
+
const byUrl = tabs.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
3105
|
+
if (byUrl.length > 0) return byUrl;
|
|
3106
|
+
return tabs.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
3107
|
+
}
|
|
3108
|
+
async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
|
|
3109
|
+
const deadline = Date.now() + budgetMs;
|
|
3110
|
+
const wanted = url.replace(/#.*$/, "");
|
|
3111
|
+
for(;;){
|
|
3112
|
+
const listed = await listBridgeTabs(projectPath, browser);
|
|
3113
|
+
if ("tabs" in listed) {
|
|
3114
|
+
for (const t of listed.tabs)if (t.url === wanted || t.url.startsWith(wanted)) return t;
|
|
3115
|
+
}
|
|
3116
|
+
if (Date.now() >= deadline) return null;
|
|
3117
|
+
await new Promise((r)=>setTimeout(r, 250));
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
3121
|
+
const expression = `(async () => { const api = typeof browser !== "undefined" ? browser : chrome; const tabs = await api.tabs.query({ active: true, currentWindow: true }); const active = tabs && tabs[0]; const tab = active && active.id != null ? await api.tabs.update(active.id, { url: ${JSON.stringify(url)} }) : await api.tabs.create({ url: ${JSON.stringify(url)} }); return { tabId: tab && tab.id != null ? tab.id : null }; })()`;
|
|
3122
|
+
const raw = await runActVerb([
|
|
3123
|
+
"eval",
|
|
3124
|
+
expression,
|
|
3125
|
+
projectPath,
|
|
3126
|
+
"--context",
|
|
3127
|
+
"background",
|
|
3128
|
+
"--browser",
|
|
3129
|
+
browser,
|
|
3130
|
+
...null != timeout ? [
|
|
3131
|
+
"--timeout",
|
|
3132
|
+
String(timeout)
|
|
3133
|
+
] : []
|
|
3134
|
+
], projectPath, timeout);
|
|
3135
|
+
try {
|
|
3136
|
+
const parsed = JSON.parse(raw);
|
|
3137
|
+
if (parsed?.ok === false) {
|
|
3138
|
+
if (!parsed.hint) parsed.hint = "On this browser family URL navigation rides the agent bridge (a background eval of tabs.update), so the dev session must be started with allowEval: true (extension_dev).";
|
|
3139
|
+
return JSON.stringify(parsed);
|
|
3140
|
+
}
|
|
3141
|
+
} catch {
|
|
3142
|
+
return raw;
|
|
3143
|
+
}
|
|
3144
|
+
const settled = await pollForBridgeTab(projectPath, browser, url, null != timeout ? Math.min(timeout, 6000) : 6000);
|
|
3145
|
+
if (!settled) return JSON.stringify({
|
|
3146
|
+
ok: false,
|
|
3147
|
+
error: {
|
|
3148
|
+
name: "NavigateFailed",
|
|
3149
|
+
message: `Navigation to ${url} did not produce a tab reporting that URL. The URL may not exist, or the browser refused the navigation (Firefox rejects privileged about:/chrome: URLs and other extensions' moz-extension: pages).`
|
|
3150
|
+
},
|
|
3151
|
+
hint: "Confirm the URL, or discover open tabs with extension_dom_inspect listTabs: true. For an extension page, the path must match the BUILT manifest."
|
|
3152
|
+
});
|
|
3153
|
+
return JSON.stringify({
|
|
3154
|
+
ok: true,
|
|
3155
|
+
navigated: url,
|
|
3156
|
+
tab: {
|
|
3157
|
+
tabId: settled.tabId,
|
|
3158
|
+
url: settled.url,
|
|
3159
|
+
title: settled.title
|
|
3160
|
+
},
|
|
3161
|
+
hint: "Inspect it with extension_dom_inspect or extension_eval using url or this numeric tab id (context: 'page'/'content')."
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
3165
|
+
const raw = await runActVerb([
|
|
3166
|
+
"eval",
|
|
3167
|
+
'(typeof browser !== "undefined" ? browser : chrome).runtime.getURL("")',
|
|
3168
|
+
projectPath,
|
|
3169
|
+
"--context",
|
|
3170
|
+
"background",
|
|
3171
|
+
"--browser",
|
|
3172
|
+
browser,
|
|
3173
|
+
...null != timeout ? [
|
|
3174
|
+
"--timeout",
|
|
3175
|
+
String(timeout)
|
|
3176
|
+
] : []
|
|
3177
|
+
], projectPath, timeout);
|
|
3178
|
+
try {
|
|
3179
|
+
const parsed = JSON.parse(raw);
|
|
3180
|
+
if (parsed?.ok && "string" == typeof parsed.value && parsed.value) return parsed.value.endsWith("/") ? parsed.value : `${parsed.value}/`;
|
|
3181
|
+
} catch {}
|
|
3182
|
+
return null;
|
|
3183
|
+
}
|
|
3015
3184
|
const source_inspect_schema = {
|
|
3016
3185
|
name: "extension_source_inspect",
|
|
3017
|
-
description: "Inspect a running extension's live state
|
|
3186
|
+
description: "Inspect a running extension's live state: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Chromium sessions ride Chrome DevTools Protocol; Firefox sessions ride the agent bridge (needs allowEval: true) and cover summary/meta/html/probes, while dom_snapshot, extension_roots, console, and deepDom stay CDP-only. Requires an active dev or start session.",
|
|
3018
3187
|
inputSchema: {
|
|
3019
3188
|
type: "object",
|
|
3020
3189
|
properties: {
|
|
@@ -3081,10 +3250,7 @@ async function source_inspect_handler(args) {
|
|
|
3081
3250
|
"console"
|
|
3082
3251
|
]);
|
|
3083
3252
|
const maxBytes = args.maxBytes ?? 262144;
|
|
3084
|
-
if (!isChromiumFamily(browser)) return
|
|
3085
|
-
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.`,
|
|
3086
|
-
hint: `For the ${browser} session, read runtime state with extension_logs, or extension_eval / extension_dom_inspect (content/page, an open surface like popup/options/sidebar, or an override page like newtab) which work against Firefox over the control channel. To get CDP DOM inspection, run a Chromium-family dev session (extension_dev with browser: "chrome") in parallel.`
|
|
3087
|
-
});
|
|
3253
|
+
if (!isChromiumFamily(browser)) return inspectViaBridge(args, browser, include, maxBytes);
|
|
3088
3254
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
3089
3255
|
if (!resolved) return JSON.stringify({
|
|
3090
3256
|
error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
|
|
@@ -3186,6 +3352,117 @@ async function source_inspect_handler(args) {
|
|
|
3186
3352
|
cdp.disconnect();
|
|
3187
3353
|
}
|
|
3188
3354
|
}
|
|
3355
|
+
function buildBridgeInspectExpression(opts) {
|
|
3356
|
+
const parts = [
|
|
3357
|
+
"const out = {};"
|
|
3358
|
+
];
|
|
3359
|
+
if (opts.meta) parts.push("try { out.meta = { url: location.href, title: document.title, readyState: document.readyState }; } catch (e) {}");
|
|
3360
|
+
if (opts.summary) parts.push(`try {
|
|
3361
|
+
const roots = document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])');
|
|
3362
|
+
out.summary = {
|
|
3363
|
+
htmlLength: document.documentElement.outerHTML.length,
|
|
3364
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
3365
|
+
styleCount: document.querySelectorAll('style').length,
|
|
3366
|
+
linkCount: document.querySelectorAll('link').length,
|
|
3367
|
+
extensionRootCount: roots.length,
|
|
3368
|
+
bodyChildCount: document.body ? document.body.children.length : 0
|
|
3369
|
+
};
|
|
3370
|
+
} catch (e) { out.summary = {}; }`);
|
|
3371
|
+
if (opts.html) parts.push(`try {
|
|
3372
|
+
const html = document.documentElement.outerHTML;
|
|
3373
|
+
const cap = ${JSON.stringify(opts.maxBytes)};
|
|
3374
|
+
out.htmlTruncated = cap > 0 && html.length > cap;
|
|
3375
|
+
out.html = out.htmlTruncated ? html.slice(0, cap) : html;
|
|
3376
|
+
} catch (e) {}`);
|
|
3377
|
+
if (opts.probes.length) parts.push(`out.probes = {};
|
|
3378
|
+
for (const sel of ${JSON.stringify(opts.probes)}) {
|
|
3379
|
+
try {
|
|
3380
|
+
const nodes = document.querySelectorAll(sel);
|
|
3381
|
+
const first = nodes[0];
|
|
3382
|
+
out.probes[sel] = { count: nodes.length, sample: first ? String(first.outerHTML || "").slice(0, 200) : null };
|
|
3383
|
+
} catch (e) { out.probes[sel] = { error: String((e && e.message) || e) }; }
|
|
3384
|
+
}`);
|
|
3385
|
+
parts.push("return out;");
|
|
3386
|
+
return `(() => { ${parts.join("\n")} })()`;
|
|
3387
|
+
}
|
|
3388
|
+
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
3389
|
+
const notes = [];
|
|
3390
|
+
const cdpOnly = [
|
|
3391
|
+
"dom_snapshot",
|
|
3392
|
+
"extension_roots"
|
|
3393
|
+
].filter((k)=>include.has(k));
|
|
3394
|
+
if (cdpOnly.length) notes.push(`${cdpOnly.join(" and ")} require CDP and are Chromium-only; on ${browser}, extension_dom_inspect's summary reports extension roots and open shadow roots.`);
|
|
3395
|
+
if (args.deepDom) notes.push("deepDom (closed shadow roots) requires CDP and is Chromium-only.");
|
|
3396
|
+
if (include.has("console")) notes.push(`Console capture rides CDP; on ${browser} read extension_logs, where the engine streams console output.`);
|
|
3397
|
+
if (args.url) {
|
|
3398
|
+
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
3399
|
+
if ("error" in listed) return listed.error;
|
|
3400
|
+
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
3401
|
+
if (!already) {
|
|
3402
|
+
const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout);
|
|
3403
|
+
try {
|
|
3404
|
+
if (JSON.parse(nav)?.ok !== true) return nav;
|
|
3405
|
+
} catch {
|
|
3406
|
+
return nav;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
const expression = buildBridgeInspectExpression({
|
|
3411
|
+
summary: include.has("summary"),
|
|
3412
|
+
meta: true,
|
|
3413
|
+
html: include.has("html"),
|
|
3414
|
+
probes: args.probe ?? [],
|
|
3415
|
+
maxBytes
|
|
3416
|
+
});
|
|
3417
|
+
const raw = await runActVerb([
|
|
3418
|
+
"eval",
|
|
3419
|
+
expression,
|
|
3420
|
+
args.projectPath,
|
|
3421
|
+
"--context",
|
|
3422
|
+
"page",
|
|
3423
|
+
...args.url ? [
|
|
3424
|
+
"--url",
|
|
3425
|
+
args.url
|
|
3426
|
+
] : [],
|
|
3427
|
+
"--browser",
|
|
3428
|
+
browser,
|
|
3429
|
+
...null != args.timeout ? [
|
|
3430
|
+
"--timeout",
|
|
3431
|
+
String(args.timeout)
|
|
3432
|
+
] : []
|
|
3433
|
+
], args.projectPath, args.timeout);
|
|
3434
|
+
let parsed;
|
|
3435
|
+
try {
|
|
3436
|
+
parsed = JSON.parse(raw);
|
|
3437
|
+
} catch {
|
|
3438
|
+
return raw;
|
|
3439
|
+
}
|
|
3440
|
+
if (parsed?.ok !== true) return raw;
|
|
3441
|
+
const value = parsed.value ?? {};
|
|
3442
|
+
const result = {
|
|
3443
|
+
browser,
|
|
3444
|
+
transport: "bridge"
|
|
3445
|
+
};
|
|
3446
|
+
if (value.meta) {
|
|
3447
|
+
result.target = {
|
|
3448
|
+
url: value.meta.url,
|
|
3449
|
+
title: value.meta.title
|
|
3450
|
+
};
|
|
3451
|
+
if (include.has("meta")) result.meta = value.meta;
|
|
3452
|
+
}
|
|
3453
|
+
if (include.has("summary") && value.summary) result.summary = value.summary;
|
|
3454
|
+
if (include.has("html") && "string" == typeof value.html) {
|
|
3455
|
+
result.html = value.html;
|
|
3456
|
+
if (value.htmlTruncated) result.htmlTruncated = true;
|
|
3457
|
+
}
|
|
3458
|
+
if (value.probes) {
|
|
3459
|
+
result.probes = value.probes;
|
|
3460
|
+
const jsLooking = (args.probe ?? []).filter((p)=>/^typeof\s|^(chrome|browser|window|document)\.|\(\)|=>|===/.test(p));
|
|
3461
|
+
if (jsLooking.length) result.probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
|
|
3462
|
+
}
|
|
3463
|
+
if (notes.length) result.notes = notes;
|
|
3464
|
+
return JSON.stringify(result);
|
|
3465
|
+
}
|
|
3189
3466
|
const list_extensions_schema = {
|
|
3190
3467
|
name: "extension_list_extensions",
|
|
3191
3468
|
description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). The entry for THIS dev session's extension (the project being served) is flagged ownExtension: true, with name and version resolved from the session's ready contract even when the browser exposes no identity. Other extensions resolve via the read-only Extensions domain when available; their contexts are never attached to or evaluated in, so an id that the domain does not describe stays id-only with a note saying why. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
|
|
@@ -3665,58 +3942,6 @@ async function logs_handler(args) {
|
|
|
3665
3942
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
3666
3943
|
return readFromFile(args, browser, limit);
|
|
3667
3944
|
}
|
|
3668
|
-
function toMcpSpeak(text) {
|
|
3669
|
-
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
3670
|
-
}
|
|
3671
|
-
function withSessionContext(message, projectPath) {
|
|
3672
|
-
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
3673
|
-
if (!isControlError) return message;
|
|
3674
|
-
const dead = deadReadySession(projectPath);
|
|
3675
|
-
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.`;
|
|
3676
|
-
const running = knownSessionBrowsers(projectPath);
|
|
3677
|
-
if (0 === running.length) return message;
|
|
3678
|
-
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.`;
|
|
3679
|
-
}
|
|
3680
|
-
function translateFrame(frame, projectPath) {
|
|
3681
|
-
if (!frame || false !== frame.ok) return frame;
|
|
3682
|
-
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
|
|
3683
|
-
if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
|
|
3684
|
-
if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
|
|
3685
|
-
return frame;
|
|
3686
|
-
}
|
|
3687
|
-
async function runActVerb(args, projectPath, timeoutMs) {
|
|
3688
|
-
const { code, stdout, stderr } = await runExtensionCli([
|
|
3689
|
-
...args,
|
|
3690
|
-
"--output",
|
|
3691
|
-
"json"
|
|
3692
|
-
], {
|
|
3693
|
-
cwd: projectPath,
|
|
3694
|
-
timeoutMs
|
|
3695
|
-
});
|
|
3696
|
-
const out = stdout.trim();
|
|
3697
|
-
if (out) try {
|
|
3698
|
-
const frame = JSON.parse(out);
|
|
3699
|
-
if (frame && false === frame.ok) return JSON.stringify(translateFrame(frame, projectPath));
|
|
3700
|
-
return out;
|
|
3701
|
-
} catch {}
|
|
3702
|
-
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
3703
|
-
return JSON.stringify({
|
|
3704
|
-
ok: false,
|
|
3705
|
-
error: {
|
|
3706
|
-
name: "CliError",
|
|
3707
|
-
message: withSessionContext(toMcpSpeak(message), projectPath)
|
|
3708
|
-
}
|
|
3709
|
-
});
|
|
3710
|
-
}
|
|
3711
|
-
function commonFlags(args) {
|
|
3712
|
-
const flags = [];
|
|
3713
|
-
if (args.context) flags.push("--context", args.context);
|
|
3714
|
-
if (args.url) flags.push("--url", args.url);
|
|
3715
|
-
if (null != args.tab) flags.push("--tab", String(args.tab));
|
|
3716
|
-
if (args.browser) flags.push("--browser", args.browser);
|
|
3717
|
-
if (null != args.timeout) flags.push("--timeout", String(args.timeout));
|
|
3718
|
-
return flags;
|
|
3719
|
-
}
|
|
3720
3945
|
const eval_schema = {
|
|
3721
3946
|
name: "extension_eval",
|
|
3722
3947
|
description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Context default: on a Chromium session whose manifest is MV3 (the default template) the default is `page` (the active tab), because the MV3 background is a service worker whose CSP blocks eval, so a background default would fail on the most common path; on Firefox/MV2 sessions the default stays `background`. Pass `context: \"background\"` explicitly to target the worker anyway (on Chromium MV3 that returns the CSP explanation). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
|
|
@@ -3984,14 +4209,8 @@ async function pollForTarget(port, url, budgetMs) {
|
|
|
3984
4209
|
await new Promise((r)=>setTimeout(r, 250));
|
|
3985
4210
|
}
|
|
3986
4211
|
}
|
|
3987
|
-
async function navigateToUrl(projectPath, browser, url) {
|
|
3988
|
-
if (!isChromiumFamily(browser)) return
|
|
3989
|
-
ok: false,
|
|
3990
|
-
error: {
|
|
3991
|
-
name: "Unsupported",
|
|
3992
|
-
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.`
|
|
3993
|
-
}
|
|
3994
|
-
});
|
|
4212
|
+
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
4213
|
+
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
3995
4214
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3996
4215
|
if (!resolved) return JSON.stringify({
|
|
3997
4216
|
ok: false,
|
|
@@ -4234,16 +4453,32 @@ async function applyPopupBounds(projectPath, browser, targetId) {
|
|
|
4234
4453
|
async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
4235
4454
|
const doc = surfaceDocument(projectPath, browser, surface);
|
|
4236
4455
|
if (!doc) return missingSurfaceError(projectPath, browser, surface, "so there is no page to render as a tab");
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4456
|
+
let url;
|
|
4457
|
+
let extensionId = null;
|
|
4458
|
+
if (isChromiumFamily(browser)) {
|
|
4459
|
+
extensionId = await resolveExtensionId(projectPath, browser);
|
|
4460
|
+
if (!extensionId) return JSON.stringify({
|
|
4461
|
+
ok: false,
|
|
4462
|
+
error: {
|
|
4463
|
+
name: "NoExtensionId",
|
|
4464
|
+
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
4465
|
+
},
|
|
4466
|
+
hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
|
|
4467
|
+
});
|
|
4468
|
+
url = `chrome-extension://${extensionId}/${doc}`;
|
|
4469
|
+
} else {
|
|
4470
|
+
const base = await resolveBridgeBaseUrl(projectPath, browser);
|
|
4471
|
+
if (!base) return JSON.stringify({
|
|
4472
|
+
ok: false,
|
|
4473
|
+
error: {
|
|
4474
|
+
name: "NoExtensionId",
|
|
4475
|
+
message: "Could not resolve the extension's moz-extension:// base URL from the live session (a background eval of runtime.getURL)."
|
|
4476
|
+
},
|
|
4477
|
+
hint: "Confirm the session is ready (extension_wait) and was started with allowEval: true (extension_dev)."
|
|
4478
|
+
});
|
|
4479
|
+
url = `${base}${doc}`;
|
|
4480
|
+
extensionId = base.replace(/^.*:\/\//, "").replace(/\/$/, "");
|
|
4481
|
+
}
|
|
4247
4482
|
const raw = await navigateToUrl(projectPath, browser, url);
|
|
4248
4483
|
try {
|
|
4249
4484
|
const parsed = JSON.parse(raw);
|
|
@@ -4251,14 +4486,14 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4251
4486
|
parsed.renderedAsTab = {
|
|
4252
4487
|
surface,
|
|
4253
4488
|
document: doc,
|
|
4254
|
-
extensionId
|
|
4489
|
+
extensionId
|
|
4255
4490
|
};
|
|
4256
4491
|
let popupBounds = null;
|
|
4257
4492
|
if (("popup" === surface || "action" === surface) && "string" == typeof parsed.target?.targetId) {
|
|
4258
4493
|
popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
|
|
4259
4494
|
if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
|
|
4260
4495
|
}
|
|
4261
|
-
parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this
|
|
4496
|
+
parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_inspect or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
|
|
4262
4497
|
return JSON.stringify(parsed);
|
|
4263
4498
|
}
|
|
4264
4499
|
} catch {}
|
|
@@ -4294,7 +4529,7 @@ const open_schema = {
|
|
|
4294
4529
|
},
|
|
4295
4530
|
url: {
|
|
4296
4531
|
type: "string",
|
|
4297
|
-
description: "Navigate a real tab to this URL (Chromium
|
|
4532
|
+
description: "Navigate a real tab to this URL instead of opening a surface (Chromium via CDP; Firefox via the agent bridge, which needs allowEval: true). Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
|
|
4298
4533
|
},
|
|
4299
4534
|
asTab: {
|
|
4300
4535
|
type: "boolean",
|
|
@@ -4317,7 +4552,7 @@ const open_schema = {
|
|
|
4317
4552
|
};
|
|
4318
4553
|
async function open_handler(args) {
|
|
4319
4554
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4320
|
-
if (args.url) return navigateToUrl(args.projectPath, browser, args.url);
|
|
4555
|
+
if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
|
|
4321
4556
|
const AS_TAB_SURFACES = [
|
|
4322
4557
|
"popup",
|
|
4323
4558
|
"options",
|
|
@@ -4368,7 +4603,7 @@ async function open_handler(args) {
|
|
|
4368
4603
|
const parsed = JSON.parse(raw);
|
|
4369
4604
|
const msg = String(parsed?.error?.message ?? "");
|
|
4370
4605
|
if (parsed?.ok === false && /active browser window|no active|headless|user gesture/i.test(msg)) {
|
|
4371
|
-
if (AS_TAB_SURFACES.includes(args.surface)
|
|
4606
|
+
if (AS_TAB_SURFACES.includes(args.surface)) {
|
|
4372
4607
|
const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
4373
4608
|
try {
|
|
4374
4609
|
const parsedFallback = JSON.parse(fallback);
|
|
@@ -4422,7 +4657,7 @@ const dom_inspect_schema = {
|
|
|
4422
4657
|
},
|
|
4423
4658
|
tabUrl: {
|
|
4424
4659
|
type: "string",
|
|
4425
|
-
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser
|
|
4660
|
+
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser BEFORE inspecting (Chromium: CDP page targets; Firefox: the agent bridge tab list): exactly one match proceeds; zero or several matches return the candidates so you can narrow, never a guess. Alternative to `url`."
|
|
4426
4661
|
},
|
|
4427
4662
|
listTargets: {
|
|
4428
4663
|
type: "boolean",
|
|
@@ -4550,6 +4785,7 @@ async function dom_inspect_handler(args) {
|
|
|
4550
4785
|
] : []
|
|
4551
4786
|
], args.projectPath, args.timeout);
|
|
4552
4787
|
let targetUrl = args.url;
|
|
4788
|
+
let targetTab = args.tab;
|
|
4553
4789
|
let resolvedTarget = null;
|
|
4554
4790
|
if (args.tabUrl) {
|
|
4555
4791
|
if (null != args.tab || args.url) return JSON.stringify({
|
|
@@ -4560,48 +4796,79 @@ async function dom_inspect_handler(args) {
|
|
|
4560
4796
|
}
|
|
4561
4797
|
});
|
|
4562
4798
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4799
|
+
if (isChromiumFamily(browser)) {
|
|
4800
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
|
|
4801
|
+
if ("error" in cdp) return cdp.error;
|
|
4802
|
+
let targets;
|
|
4803
|
+
try {
|
|
4804
|
+
targets = await listPageTargets(cdp.port);
|
|
4805
|
+
} catch (e) {
|
|
4806
|
+
return JSON.stringify({
|
|
4807
|
+
ok: false,
|
|
4808
|
+
error: {
|
|
4809
|
+
name: "CdpError",
|
|
4810
|
+
message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
|
|
4811
|
+
},
|
|
4812
|
+
hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4816
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4570
4817
|
ok: false,
|
|
4571
4818
|
error: {
|
|
4572
|
-
name: "
|
|
4573
|
-
message: `
|
|
4819
|
+
name: "NoMatchingTarget",
|
|
4820
|
+
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4574
4821
|
},
|
|
4575
|
-
|
|
4822
|
+
availableTargets: targets,
|
|
4823
|
+
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
4576
4824
|
});
|
|
4825
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4826
|
+
ok: false,
|
|
4827
|
+
error: {
|
|
4828
|
+
name: "AmbiguousTabUrl",
|
|
4829
|
+
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4830
|
+
},
|
|
4831
|
+
matchingTargets: matches,
|
|
4832
|
+
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4833
|
+
});
|
|
4834
|
+
resolvedTarget = {
|
|
4835
|
+
...matches[0]
|
|
4836
|
+
};
|
|
4837
|
+
targetUrl = matches[0].url;
|
|
4838
|
+
} else {
|
|
4839
|
+
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
4840
|
+
if ("error" in listed) return listed.error;
|
|
4841
|
+
const matches = matchTabsByUrl(listed.tabs, args.tabUrl);
|
|
4842
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4843
|
+
ok: false,
|
|
4844
|
+
error: {
|
|
4845
|
+
name: "NoMatchingTarget",
|
|
4846
|
+
message: `No open tab's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4847
|
+
},
|
|
4848
|
+
availableTabs: listed.tabs,
|
|
4849
|
+
hint: "Pick one from availableTabs and retry with a `tabUrl` substring of its url, or open the page first (extension_open with `url`)."
|
|
4850
|
+
});
|
|
4851
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4852
|
+
ok: false,
|
|
4853
|
+
error: {
|
|
4854
|
+
name: "AmbiguousTabUrl",
|
|
4855
|
+
message: `${matches.length} tabs match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4856
|
+
},
|
|
4857
|
+
matchingTabs: matches,
|
|
4858
|
+
hint: "Narrow `tabUrl` to a longer substring that matches exactly one url in matchingTabs, or pass its numeric tabId as `tab`."
|
|
4859
|
+
});
|
|
4860
|
+
resolvedTarget = {
|
|
4861
|
+
...matches[0]
|
|
4862
|
+
};
|
|
4863
|
+
if (null != matches[0].tabId) targetTab = matches[0].tabId;
|
|
4864
|
+
else targetUrl = matches[0].url;
|
|
4577
4865
|
}
|
|
4578
|
-
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4579
|
-
if (0 === matches.length) return JSON.stringify({
|
|
4580
|
-
ok: false,
|
|
4581
|
-
error: {
|
|
4582
|
-
name: "NoMatchingTarget",
|
|
4583
|
-
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4584
|
-
},
|
|
4585
|
-
availableTargets: targets,
|
|
4586
|
-
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
4587
|
-
});
|
|
4588
|
-
if (matches.length > 1) return JSON.stringify({
|
|
4589
|
-
ok: false,
|
|
4590
|
-
error: {
|
|
4591
|
-
name: "AmbiguousTabUrl",
|
|
4592
|
-
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4593
|
-
},
|
|
4594
|
-
matchingTargets: matches,
|
|
4595
|
-
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4596
|
-
});
|
|
4597
|
-
resolvedTarget = matches[0];
|
|
4598
|
-
targetUrl = resolvedTarget.url;
|
|
4599
4866
|
}
|
|
4600
4867
|
const cli = [
|
|
4601
4868
|
"inspect",
|
|
4602
4869
|
args.projectPath
|
|
4603
4870
|
];
|
|
4604
|
-
if (null !=
|
|
4871
|
+
if (null != targetTab) cli.push("--tab", String(targetTab));
|
|
4605
4872
|
if (targetUrl) cli.push("--url", targetUrl);
|
|
4606
4873
|
if (args.context) cli.push("--context", args.context);
|
|
4607
4874
|
if (args.include?.length) cli.push("--include", args.include.join(","));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface BridgeTab {
|
|
2
|
+
tabId: number | null;
|
|
3
|
+
url: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function listBridgeTabs(projectPath: string, browser: string, timeout?: number): Promise<{
|
|
7
|
+
tabs: BridgeTab[];
|
|
8
|
+
} | {
|
|
9
|
+
error: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function matchTabsByUrl(tabs: BridgeTab[], needle: string): BridgeTab[];
|
|
12
|
+
export declare function pollForBridgeTab(projectPath: string, browser: string, url: string, budgetMs: number): Promise<BridgeTab | null>;
|
|
13
|
+
export declare function navigateToUrlViaBridge(projectPath: string, browser: string, url: string, timeout?: number): Promise<string>;
|
|
14
|
+
export declare function resolveBridgeBaseUrl(projectPath: string, browser: string, timeout?: number): Promise<string | null>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.5.
|
|
5
|
-
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 33 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox,
|
|
4
|
+
"version": "5.5.2",
|
|
5
|
+
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 33 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf), with Safari coming next. Powered by extension.dev and Extension.js.",
|
|
6
6
|
"mcpName": "io.github.extensiondev/mcp",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": {
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "5.5.
|
|
10
|
+
"version": "5.5.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@extension.dev/mcp",
|
|
16
|
-
"version": "5.5.
|
|
16
|
+
"version": "5.5.2",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|