@extension.dev/mcp 5.4.0 → 5.5.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 +40 -0
- package/dist/module.js +239 -13
- package/dist/src/lib/cdp-targets.d.ts +15 -0
- package/dist/src/tools/dom-inspect.d.ts +11 -0
- package/dist/src/tools/eval.d.ts +1 -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": "5.
|
|
13
|
+
"version": "5.5.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": "5.
|
|
4
|
+
"version": "5.5.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "boss@cezaraugusto.net",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.5.0
|
|
4
|
+
|
|
5
|
+
The debug surfaces learn to point at things: tabs are targetable by
|
|
6
|
+
URL, extensions in lists have names, and eval works on the default
|
|
7
|
+
template by default.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `extension_dom_inspect` targets tabs by `tabUrl` (case-insensitive
|
|
12
|
+
URL substring, title as fallback). Exactly one match inspects it and
|
|
13
|
+
the result names the resolved target; zero or multiple matches return
|
|
14
|
+
the candidate targets instead of guessing. `listTargets: true` lists
|
|
15
|
+
the browser's live CDP page targets (targetId, url, title, type) with
|
|
16
|
+
the standing warning that a CDP targetId is NOT a chrome.tabs id.
|
|
17
|
+
- `extension_list_extensions` names its entries. The dev session's own
|
|
18
|
+
extension resolves to `name`, `version`, and `ownExtension: true`
|
|
19
|
+
(identified by recomputing Chrome's unpacked-extension id from the
|
|
20
|
+
ready contract's distPath) and sorts first; entries that cannot be
|
|
21
|
+
resolved carry a note saying why instead of a silent bare id. Other
|
|
22
|
+
extensions' contexts are never attached to or evaluated in.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `extension_eval` works on the default template by default. On
|
|
27
|
+
Chromium with an MV3 manifest the default context is now `page` (the
|
|
28
|
+
MV3 service worker CSP blocks background eval), disclosed in the
|
|
29
|
+
result as `defaultedContext` with the reason; explicit
|
|
30
|
+
`context: "background"` is unchanged and keeps its CSP explanation.
|
|
31
|
+
Firefox and MV2 defaults are untouched. A defaulted eval that lands
|
|
32
|
+
on an unreachable active tab returns a hint to navigate or pass
|
|
33
|
+
`url`/`tab`.
|
|
34
|
+
- Error remedies speak tool arguments, never CLI flags: the engine's
|
|
35
|
+
`--context page --tab <id>` prose is rewritten into `context:`/
|
|
36
|
+
`tab:`/`url:` vocabulary, with guards so ordinary prose is never
|
|
37
|
+
garbled by the rewriting.
|
|
38
|
+
- The debugging documentation's headline example now runs on the
|
|
39
|
+
default template (page-context eval); the background variant is
|
|
40
|
+
labeled MV2/Firefox, and the cross-browser matrix states per-context
|
|
41
|
+
eval support honestly with the MV3 CSP footnote.
|
|
42
|
+
|
|
3
43
|
## 5.4.0
|
|
4
44
|
|
|
5
45
|
The DevX swarm's non-blocker friction clusters, cleared: waiting is
|
package/dist/module.js
CHANGED
|
@@ -68,6 +68,7 @@ var eval_namespaceObject = {};
|
|
|
68
68
|
__webpack_require__.r(eval_namespaceObject);
|
|
69
69
|
__webpack_require__.d(eval_namespaceObject, {
|
|
70
70
|
handler: ()=>eval_handler,
|
|
71
|
+
resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
|
|
71
72
|
schema: ()=>eval_schema
|
|
72
73
|
});
|
|
73
74
|
var get_template_source_namespaceObject = {};
|
|
@@ -210,7 +211,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
210
211
|
handler: ()=>whoami_handler,
|
|
211
212
|
schema: ()=>whoami_schema
|
|
212
213
|
});
|
|
213
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
214
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.5.0","El":{"OP":"^4.0.13"}}');
|
|
214
215
|
function scaffoldEnginePin(projectPath) {
|
|
215
216
|
try {
|
|
216
217
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -3179,7 +3180,7 @@ async function source_inspect_handler(args) {
|
|
|
3179
3180
|
}
|
|
3180
3181
|
const list_extensions_schema = {
|
|
3181
3182
|
name: "extension_list_extensions",
|
|
3182
|
-
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).
|
|
3183
|
+
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.",
|
|
3183
3184
|
inputSchema: {
|
|
3184
3185
|
type: "object",
|
|
3185
3186
|
properties: {
|
|
@@ -3197,6 +3198,47 @@ const list_extensions_schema = {
|
|
|
3197
3198
|
]
|
|
3198
3199
|
}
|
|
3199
3200
|
};
|
|
3201
|
+
function unpackedExtensionId(distPath) {
|
|
3202
|
+
const digest = node_crypto.createHash("sha256").update(distPath).digest();
|
|
3203
|
+
let id = "";
|
|
3204
|
+
for(let i = 0; i < 16; i++){
|
|
3205
|
+
id += String.fromCharCode(97 + (digest[i] >> 4));
|
|
3206
|
+
id += String.fromCharCode(97 + (0x0f & digest[i]));
|
|
3207
|
+
}
|
|
3208
|
+
return id;
|
|
3209
|
+
}
|
|
3210
|
+
function readOwnIdentity(projectPath, browser) {
|
|
3211
|
+
let contract;
|
|
3212
|
+
try {
|
|
3213
|
+
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3214
|
+
contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3215
|
+
} catch {
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
const distPath = "string" == typeof contract?.distPath ? contract.distPath : null;
|
|
3219
|
+
const ids = [];
|
|
3220
|
+
if (distPath) {
|
|
3221
|
+
ids.push(unpackedExtensionId(distPath));
|
|
3222
|
+
try {
|
|
3223
|
+
const real = node_fs.realpathSync(distPath);
|
|
3224
|
+
if (real !== distPath) ids.push(unpackedExtensionId(real));
|
|
3225
|
+
} catch {}
|
|
3226
|
+
}
|
|
3227
|
+
let name = "string" == typeof contract?.extensionName ? contract.extensionName : void 0;
|
|
3228
|
+
let version = "string" == typeof contract?.extensionVersion ? contract.extensionVersion : void 0;
|
|
3229
|
+
if ((!name || !version) && distPath) try {
|
|
3230
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(distPath, "manifest.json"), "utf8"));
|
|
3231
|
+
if (!name && "string" == typeof manifest?.name && !manifest.name.startsWith("__MSG_")) name = manifest.name;
|
|
3232
|
+
if (!version && "string" == typeof manifest?.version) version = manifest.version;
|
|
3233
|
+
} catch {}
|
|
3234
|
+
if (0 === ids.length && !name) return null;
|
|
3235
|
+
return {
|
|
3236
|
+
ids,
|
|
3237
|
+
name,
|
|
3238
|
+
version
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domain returned nothing for this id, and other extensions' contexts are never attached to or evaluated in to read a manifest.";
|
|
3200
3242
|
async function list_extensions_handler(args) {
|
|
3201
3243
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3202
3244
|
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
@@ -3237,6 +3279,7 @@ async function list_extensions_handler(args) {
|
|
|
3237
3279
|
});
|
|
3238
3280
|
byId.set(id, list);
|
|
3239
3281
|
}
|
|
3282
|
+
const own = readOwnIdentity(args.projectPath, browser);
|
|
3240
3283
|
const extensions = [];
|
|
3241
3284
|
for (const [id, ctxTargets] of byId){
|
|
3242
3285
|
const entry = {
|
|
@@ -3257,15 +3300,33 @@ async function list_extensions_handler(args) {
|
|
|
3257
3300
|
entry.source = "extensions-domain";
|
|
3258
3301
|
}
|
|
3259
3302
|
} catch {}
|
|
3303
|
+
if (own?.ids.includes(id)) {
|
|
3304
|
+
entry.ownExtension = true;
|
|
3305
|
+
if (void 0 === entry.name && void 0 !== own.name) {
|
|
3306
|
+
entry.name = own.name;
|
|
3307
|
+
if (void 0 !== own.version) entry.version = own.version;
|
|
3308
|
+
entry.source = "session-contract";
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3260
3311
|
extensions.push(entry);
|
|
3261
3312
|
}
|
|
3262
|
-
|
|
3313
|
+
if (own?.name && !extensions.some((e)=>e.ownExtension)) {
|
|
3314
|
+
const byName = extensions.filter((e)=>e.name === own.name);
|
|
3315
|
+
if (1 === byName.length) byName[0].ownExtension = true;
|
|
3316
|
+
}
|
|
3317
|
+
for (const entry of extensions)if (void 0 === entry.name) entry.note = UNRESOLVED_NOTE;
|
|
3318
|
+
extensions.sort((a, b)=>{
|
|
3319
|
+
if ((a.ownExtension ?? false) !== (b.ownExtension ?? false)) return a.ownExtension ? -1 : 1;
|
|
3320
|
+
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
3321
|
+
});
|
|
3322
|
+
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
3263
3323
|
return JSON.stringify({
|
|
3264
3324
|
cdpPort,
|
|
3265
3325
|
browser,
|
|
3266
3326
|
count: extensions.length,
|
|
3327
|
+
ownExtensionId: ownEntry?.id ?? null,
|
|
3267
3328
|
extensions,
|
|
3268
|
-
note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes.
|
|
3329
|
+
note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. ownExtension marks the extension this dev session serves, identified from the session's ready contract. Other identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
|
|
3269
3330
|
});
|
|
3270
3331
|
} catch (error) {
|
|
3271
3332
|
return JSON.stringify({
|
|
@@ -3597,7 +3658,7 @@ async function logs_handler(args) {
|
|
|
3597
3658
|
return readFromFile(args, browser, limit);
|
|
3598
3659
|
}
|
|
3599
3660
|
function toMcpSpeak(text) {
|
|
3600
|
-
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[= ](
|
|
3661
|
+
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`");
|
|
3601
3662
|
}
|
|
3602
3663
|
function withSessionContext(message, projectPath) {
|
|
3603
3664
|
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
@@ -3650,7 +3711,7 @@ function commonFlags(args) {
|
|
|
3650
3711
|
}
|
|
3651
3712
|
const eval_schema = {
|
|
3652
3713
|
name: "extension_eval",
|
|
3653
|
-
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). 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).
|
|
3714
|
+
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`.",
|
|
3654
3715
|
inputSchema: {
|
|
3655
3716
|
type: "object",
|
|
3656
3717
|
properties: {
|
|
@@ -3676,8 +3737,7 @@ const eval_schema = {
|
|
|
3676
3737
|
"content",
|
|
3677
3738
|
"page"
|
|
3678
3739
|
],
|
|
3679
|
-
default: "background"
|
|
3680
|
-
description: "Which extension surface to evaluate in"
|
|
3740
|
+
description: "Which extension surface to evaluate in. Default: `background`, EXCEPT on Chromium sessions whose manifest is MV3, where the default is `page` (the active tab) because the MV3 service worker CSP blocks eval; pass `context: \"background\"` explicitly to target the worker anyway."
|
|
3681
3741
|
},
|
|
3682
3742
|
url: {
|
|
3683
3743
|
type: "string",
|
|
@@ -3702,14 +3762,38 @@ const eval_schema = {
|
|
|
3702
3762
|
]
|
|
3703
3763
|
}
|
|
3704
3764
|
};
|
|
3765
|
+
function resolveDefaultEvalContext(projectPath, browser) {
|
|
3766
|
+
if (!isChromiumFamily(browser)) return "background";
|
|
3767
|
+
const candidates = [
|
|
3768
|
+
node_path.join(projectPath, "dist", browser, "manifest.json"),
|
|
3769
|
+
node_path.join(projectPath, "dist", "manifest.json"),
|
|
3770
|
+
node_path.join(projectPath, "src", "manifest.json"),
|
|
3771
|
+
node_path.join(projectPath, "manifest.json")
|
|
3772
|
+
];
|
|
3773
|
+
for (const file of candidates){
|
|
3774
|
+
let manifest;
|
|
3775
|
+
try {
|
|
3776
|
+
manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3777
|
+
} catch {
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
const version = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
|
|
3781
|
+
if (3 === version) return "page";
|
|
3782
|
+
if (2 === version) break;
|
|
3783
|
+
}
|
|
3784
|
+
return "background";
|
|
3785
|
+
}
|
|
3705
3786
|
async function eval_handler(args) {
|
|
3706
3787
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
3788
|
+
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
3789
|
+
const context = defaulted ? "page" : args.context;
|
|
3707
3790
|
const raw = await runActVerb([
|
|
3708
3791
|
"eval",
|
|
3709
3792
|
args.expression,
|
|
3710
3793
|
args.projectPath,
|
|
3711
3794
|
...commonFlags({
|
|
3712
3795
|
...args,
|
|
3796
|
+
context,
|
|
3713
3797
|
browser
|
|
3714
3798
|
})
|
|
3715
3799
|
], args.projectPath, args.timeout);
|
|
@@ -3720,6 +3804,15 @@ async function eval_handler(args) {
|
|
|
3720
3804
|
return JSON.stringify(parsed);
|
|
3721
3805
|
}
|
|
3722
3806
|
} catch {}
|
|
3807
|
+
if (defaulted) try {
|
|
3808
|
+
const parsed = JSON.parse(raw);
|
|
3809
|
+
if (parsed && "object" == typeof parsed) {
|
|
3810
|
+
parsed.defaultedContext = "page";
|
|
3811
|
+
parsed.contextNote = 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).';
|
|
3812
|
+
if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_inspect with listTabs: true lists open tabs.";
|
|
3813
|
+
return JSON.stringify(parsed);
|
|
3814
|
+
}
|
|
3815
|
+
} catch {}
|
|
3723
3816
|
return raw;
|
|
3724
3817
|
}
|
|
3725
3818
|
const storage_schema = {
|
|
@@ -3948,7 +4041,7 @@ async function navigateToUrl(projectPath, browser, url) {
|
|
|
3948
4041
|
} catch {}
|
|
3949
4042
|
}
|
|
3950
4043
|
}
|
|
3951
|
-
function
|
|
4044
|
+
function open_unpackedExtensionId(distPath) {
|
|
3952
4045
|
const digest = node_crypto.createHash("sha256").update(distPath).digest();
|
|
3953
4046
|
let id = "";
|
|
3954
4047
|
for(let i = 0; i < 16; i++){
|
|
@@ -3959,7 +4052,7 @@ function unpackedExtensionId(distPath) {
|
|
|
3959
4052
|
}
|
|
3960
4053
|
async function resolveExtensionId(projectPath, browser) {
|
|
3961
4054
|
const distPath = readDistPath(projectPath, browser);
|
|
3962
|
-
const computed = distPath ?
|
|
4055
|
+
const computed = distPath ? open_unpackedExtensionId(distPath) : null;
|
|
3963
4056
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3964
4057
|
if (!resolved) return computed;
|
|
3965
4058
|
const ids = new Set();
|
|
@@ -4283,9 +4376,27 @@ async function open_handler(args) {
|
|
|
4283
4376
|
} catch {}
|
|
4284
4377
|
return raw;
|
|
4285
4378
|
}
|
|
4379
|
+
const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_inspect with listTabs: true.";
|
|
4380
|
+
function filterPageTargets(raw) {
|
|
4381
|
+
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
4382
|
+
targetId: String(t.id),
|
|
4383
|
+
type: String(t.type),
|
|
4384
|
+
url: String(t.url ?? ""),
|
|
4385
|
+
title: String(t.title ?? "")
|
|
4386
|
+
}));
|
|
4387
|
+
}
|
|
4388
|
+
async function listPageTargets(port) {
|
|
4389
|
+
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
4390
|
+
}
|
|
4391
|
+
function matchTargetsByUrl(targets, needle) {
|
|
4392
|
+
const wanted = needle.toLowerCase();
|
|
4393
|
+
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
4394
|
+
if (byUrl.length > 0) return byUrl;
|
|
4395
|
+
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
4396
|
+
}
|
|
4286
4397
|
const dom_inspect_schema = {
|
|
4287
4398
|
name: "extension_dom_inspect",
|
|
4288
|
-
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4399
|
+
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (CDP targetIds) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4289
4400
|
inputSchema: {
|
|
4290
4401
|
type: "object",
|
|
4291
4402
|
properties: {
|
|
@@ -4301,6 +4412,15 @@ const dom_inspect_schema = {
|
|
|
4301
4412
|
type: "string",
|
|
4302
4413
|
description: "For content/page: selects the target tab by url (match pattern, then substring fallback). Preferred over `tab`."
|
|
4303
4414
|
},
|
|
4415
|
+
tabUrl: {
|
|
4416
|
+
type: "string",
|
|
4417
|
+
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser's CDP page targets BEFORE inspecting: exactly one match proceeds; zero or several matches return the candidate targets (targetId/url/title) so you can narrow, never a guess. Chromium sessions only; on Firefox use `url`/`tab`. Alternative to `url`."
|
|
4418
|
+
},
|
|
4419
|
+
listTargets: {
|
|
4420
|
+
type: "boolean",
|
|
4421
|
+
default: false,
|
|
4422
|
+
description: "Enumerate the browser's CDP page targets as {targetId,url,title,type} and return, ignoring the other args. The discovery path for `tabUrl`. targetId is a CDP target id, NOT a numeric chrome.tabs id (for those use listTabs)."
|
|
4423
|
+
},
|
|
4304
4424
|
listTabs: {
|
|
4305
4425
|
type: "boolean",
|
|
4306
4426
|
default: false,
|
|
@@ -4361,8 +4481,55 @@ const dom_inspect_schema = {
|
|
|
4361
4481
|
]
|
|
4362
4482
|
}
|
|
4363
4483
|
};
|
|
4484
|
+
async function cdpPortOrError(projectPath, browser, feature) {
|
|
4485
|
+
if (!isChromiumFamily(browser)) return {
|
|
4486
|
+
error: JSON.stringify({
|
|
4487
|
+
ok: false,
|
|
4488
|
+
error: {
|
|
4489
|
+
name: "Unsupported",
|
|
4490
|
+
message: `${feature} reads the browser's CDP page targets, which ${browser} (Gecko) does not expose. Target the tab with \`url\` or \`tab\` instead, and discover tabs with listTabs: true (agent bridge, works on every browser).`
|
|
4491
|
+
}
|
|
4492
|
+
})
|
|
4493
|
+
};
|
|
4494
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
4495
|
+
if (!resolved) return {
|
|
4496
|
+
error: JSON.stringify({
|
|
4497
|
+
ok: false,
|
|
4498
|
+
error: {
|
|
4499
|
+
name: "NoSession",
|
|
4500
|
+
message: `No active dev session / CDP port for ${browser}, so ${feature} has no browser to ask. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
4501
|
+
}
|
|
4502
|
+
})
|
|
4503
|
+
};
|
|
4504
|
+
return {
|
|
4505
|
+
port: resolved.port
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4364
4508
|
async function dom_inspect_handler(args) {
|
|
4365
4509
|
const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
|
|
4510
|
+
if (args.listTargets) {
|
|
4511
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4512
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "listTargets");
|
|
4513
|
+
if ("error" in cdp) return cdp.error;
|
|
4514
|
+
try {
|
|
4515
|
+
const targets = await listPageTargets(cdp.port);
|
|
4516
|
+
return JSON.stringify({
|
|
4517
|
+
ok: true,
|
|
4518
|
+
browser,
|
|
4519
|
+
targets,
|
|
4520
|
+
note: TARGET_ID_NOTE
|
|
4521
|
+
});
|
|
4522
|
+
} catch (e) {
|
|
4523
|
+
return JSON.stringify({
|
|
4524
|
+
ok: false,
|
|
4525
|
+
error: {
|
|
4526
|
+
name: "CdpError",
|
|
4527
|
+
message: `Could not list page targets: ${e instanceof Error ? e.message : String(e)}`
|
|
4528
|
+
},
|
|
4529
|
+
hint: "Confirm the session is ready (extension_wait), then retry. listTabs: true is the CDP-free alternative."
|
|
4530
|
+
});
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4366
4533
|
if (args.listTabs) return runActVerb([
|
|
4367
4534
|
"inspect",
|
|
4368
4535
|
args.projectPath,
|
|
@@ -4374,19 +4541,78 @@ async function dom_inspect_handler(args) {
|
|
|
4374
4541
|
String(args.timeout)
|
|
4375
4542
|
] : []
|
|
4376
4543
|
], args.projectPath, args.timeout);
|
|
4544
|
+
let targetUrl = args.url;
|
|
4545
|
+
let resolvedTarget = null;
|
|
4546
|
+
if (args.tabUrl) {
|
|
4547
|
+
if (null != args.tab || args.url) return JSON.stringify({
|
|
4548
|
+
ok: false,
|
|
4549
|
+
error: {
|
|
4550
|
+
name: "BadRequest",
|
|
4551
|
+
message: "Pass ONE tab selector: `tabUrl` (URL substring, resolved against live targets), `url` (engine-side match), or `tab` (numeric chrome.tabs id), not several."
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4555
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
|
|
4556
|
+
if ("error" in cdp) return cdp.error;
|
|
4557
|
+
let targets;
|
|
4558
|
+
try {
|
|
4559
|
+
targets = await listPageTargets(cdp.port);
|
|
4560
|
+
} catch (e) {
|
|
4561
|
+
return JSON.stringify({
|
|
4562
|
+
ok: false,
|
|
4563
|
+
error: {
|
|
4564
|
+
name: "CdpError",
|
|
4565
|
+
message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
|
|
4566
|
+
},
|
|
4567
|
+
hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4571
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4572
|
+
ok: false,
|
|
4573
|
+
error: {
|
|
4574
|
+
name: "NoMatchingTarget",
|
|
4575
|
+
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4576
|
+
},
|
|
4577
|
+
availableTargets: targets,
|
|
4578
|
+
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}`
|
|
4579
|
+
});
|
|
4580
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4581
|
+
ok: false,
|
|
4582
|
+
error: {
|
|
4583
|
+
name: "AmbiguousTabUrl",
|
|
4584
|
+
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4585
|
+
},
|
|
4586
|
+
matchingTargets: matches,
|
|
4587
|
+
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4588
|
+
});
|
|
4589
|
+
resolvedTarget = matches[0];
|
|
4590
|
+
targetUrl = resolvedTarget.url;
|
|
4591
|
+
}
|
|
4377
4592
|
const cli = [
|
|
4378
4593
|
"inspect",
|
|
4379
4594
|
args.projectPath
|
|
4380
4595
|
];
|
|
4381
4596
|
if (null != args.tab) cli.push("--tab", String(args.tab));
|
|
4382
|
-
if (
|
|
4597
|
+
if (targetUrl) cli.push("--url", targetUrl);
|
|
4383
4598
|
if (args.context) cli.push("--context", args.context);
|
|
4384
4599
|
if (args.include?.length) cli.push("--include", args.include.join(","));
|
|
4385
4600
|
if (null != args.maxBytes) cli.push("--max-bytes", String(args.maxBytes));
|
|
4386
4601
|
if (null != withConsole) cli.push("--with-console", String(withConsole));
|
|
4387
4602
|
cli.push("--browser", resolveSessionBrowser(args.projectPath, args.browser).browser);
|
|
4388
4603
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
4389
|
-
|
|
4604
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
4605
|
+
if (!resolvedTarget) return raw;
|
|
4606
|
+
try {
|
|
4607
|
+
const parsed = JSON.parse(raw);
|
|
4608
|
+
parsed.resolvedTarget = {
|
|
4609
|
+
...resolvedTarget,
|
|
4610
|
+
matchedBy: "tabUrl"
|
|
4611
|
+
};
|
|
4612
|
+
return JSON.stringify(parsed);
|
|
4613
|
+
} catch {
|
|
4614
|
+
return raw;
|
|
4615
|
+
}
|
|
4390
4616
|
}
|
|
4391
4617
|
function credentialsPath() {
|
|
4392
4618
|
if ("win32" === process.platform) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PageTarget {
|
|
2
|
+
targetId: string;
|
|
3
|
+
type: string;
|
|
4
|
+
url: string;
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const TARGET_ID_NOTE: string;
|
|
8
|
+
export declare function filterPageTargets(raw: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
url: string;
|
|
12
|
+
title: string;
|
|
13
|
+
}>): PageTarget[];
|
|
14
|
+
export declare function listPageTargets(port: number): Promise<PageTarget[]>;
|
|
15
|
+
export declare function matchTargetsByUrl(targets: PageTarget[], needle: string): PageTarget[];
|
|
@@ -17,6 +17,15 @@ export declare const schema: {
|
|
|
17
17
|
type: string;
|
|
18
18
|
description: string;
|
|
19
19
|
};
|
|
20
|
+
tabUrl: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
listTargets: {
|
|
25
|
+
type: string;
|
|
26
|
+
default: boolean;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
20
29
|
listTabs: {
|
|
21
30
|
type: string;
|
|
22
31
|
default: boolean;
|
|
@@ -60,6 +69,8 @@ export declare const schema: {
|
|
|
60
69
|
export declare function handler(args: ActArgs & {
|
|
61
70
|
tab?: number;
|
|
62
71
|
url?: string;
|
|
72
|
+
tabUrl?: string;
|
|
73
|
+
listTargets?: boolean;
|
|
63
74
|
listTabs?: boolean;
|
|
64
75
|
include?: string[];
|
|
65
76
|
maxBytes?: number;
|
package/dist/src/tools/eval.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ export declare const schema: {
|
|
|
16
16
|
context: {
|
|
17
17
|
type: string;
|
|
18
18
|
enum: string[];
|
|
19
|
-
default: string;
|
|
20
19
|
description: string;
|
|
21
20
|
};
|
|
22
21
|
url: {
|
|
@@ -39,6 +38,7 @@ export declare const schema: {
|
|
|
39
38
|
required: string[];
|
|
40
39
|
};
|
|
41
40
|
};
|
|
41
|
+
export declare function resolveDefaultEvalContext(projectPath: string, browser: string): "background" | "page";
|
|
42
42
|
export declare function handler(args: ActArgs & {
|
|
43
43
|
expression: string;
|
|
44
44
|
}): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.5.0",
|
|
5
5
|
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 32 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": "5.
|
|
10
|
+
"version": "5.5.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": "5.
|
|
16
|
+
"version": "5.5.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|