@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.
@@ -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.4.0",
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.0",
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.4.0","El":{"OP":"^4.0.13"}}');
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). Identity is read read-only via the Extensions domain, other extensions' contexts are never attached to or evaluated in. 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
+ 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
- extensions.sort((a, b)=>(a.name ?? a.id).localeCompare(b.name ?? b.id));
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. Identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
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[= ]([\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");
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). Chromium caveat: eval in the MV3 background/service_worker is blocked by CSP (use an MV2/Firefox build for that context). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
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 unpackedExtensionId(distPath) {
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 ? unpackedExtensionId(distPath) : null;
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 (args.url) cli.push("--url", args.url);
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
- return runActVerb(cli, args.projectPath, args.timeout);
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;
@@ -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.0",
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.4.0",
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.4.0",
16
+ "version": "5.5.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"