@extension.dev/mcp 5.7.0 → 6.0.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.7.0",
13
+ "version": "6.0.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.7.0",
4
+ "version": "6.0.0",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,10 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## 5.7.0
3
+ ## 6.0.0
4
4
 
5
- The live-preview carrier stops living in your project, reports what it
6
- refused, and rides an engine that tells you when the browser turned your
7
- extension away.
5
+ The server moves to Apache-2.0, and the live-preview carrier stops living
6
+ in your project, reports what it refused, and rides an engine that tells
7
+ you when the browser turned your extension away.
8
+
9
+ ### Changed
10
+
11
+ - **License: MIT is now Apache-2.0.** Everything published up to and
12
+ including 5.6.1 was released under MIT and stays MIT forever; you keep
13
+ those rights on those versions. From 6.0.0 forward the license is
14
+ Apache-2.0, which adds an express patent grant and requires anyone
15
+ shipping a modified copy to state that they changed the files. For almost
16
+ every user this changes nothing about what you are allowed to do.
8
17
 
9
18
  ### Fixed
10
19
 
@@ -55,6 +64,15 @@ extension away.
55
64
  again: the platform's Safari/App Store submission lane is now enabled
56
65
  for every project, so the store enum and the per-store status report
57
66
  treat it like chrome, firefox, and edge.
67
+ - `extension_list_templates` and `extension_get_template_source` resolve
68
+ the template catalog and sources from the pinned, content-addressed
69
+ corpus served at `media.extension.land`, with a commit-pinned GitHub
70
+ raw fallback. Both tools previously read a floating `nightly`/`main`
71
+ ref, so two runs could disagree; they now resolve one immutable
72
+ release whose files are sha256-verified at the origin.
73
+ - Console and dashboard links come from a shared URL contract and are
74
+ environment aware, so a local or development run no longer hands back
75
+ hardcoded production console URLs.
58
76
 
59
77
  ### Added
60
78
 
package/LICENSE CHANGED
@@ -1,21 +1,201 @@
1
- MIT License
2
-
3
- Copyright (c) Cezar Augusto and the extension.dev collaborators
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Cezar Augusto and the extension.dev collaborators
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -156,4 +156,4 @@ All of it rides on [Extension.js](https://github.com/extension-js/extension.js),
156
156
 
157
157
  ## License
158
158
 
159
- MIT (c) Cezar Augusto and the extension.dev collaborators
159
+ Apache-2.0 (c) 2026 Cezar Augusto and the extension.dev collaborators. See [LICENSE](LICENSE).
@@ -5,7 +5,7 @@
5
5
  // ██║╚██╔╝██║██║ ██╔═══╝
6
6
  // ██║ ╚═╝ ██║╚██████╗██║
7
7
  // ╚═╝ ╚═╝ ╚═════╝╚═╝
8
- // MIT License (c) Cezar Augusto and the extension.dev collaborators
8
+ // Apache License 2.0 (c) 2026 Cezar Augusto and the extension.dev collaborators
9
9
  import {startServer, runCli} from '../dist/module.js'
10
10
 
11
11
  const [, , cmd, ...rest] = process.argv
package/dist/module.js CHANGED
@@ -138,7 +138,8 @@ __webpack_require__.r(open_namespaceObject);
138
138
  __webpack_require__.d(open_namespaceObject, {
139
139
  clampPopupBounds: ()=>clampPopupBounds,
140
140
  handler: ()=>open_handler,
141
- schema: ()=>open_schema
141
+ schema: ()=>open_schema,
142
+ sessionIsHeadless: ()=>sessionIsHeadless
142
143
  });
143
144
  var preview_namespaceObject = {};
144
145
  __webpack_require__.r(preview_namespaceObject);
@@ -388,10 +389,85 @@ async function create_handler(args) {
388
389
  }
389
390
  });
390
391
  }
392
+ const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
393
+ const DEFAULT_CHANNEL = "latest";
394
+ const PINNED_COMMIT = "2d2ed9668cca002148d9eecd953a08b54d0bad9d";
395
+ const CHANNEL_CACHE_TTL_MS = 300000;
396
+ function mediaOrigin() {
397
+ return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
398
+ }
399
+ function channelName() {
400
+ return (process.env.EXTENSION_TEMPLATES_CHANNEL || "").trim() || DEFAULT_CHANNEL;
401
+ }
402
+ function pinnedCommitOverride() {
403
+ return (process.env.EXTENSION_TEMPLATES_COMMIT || "").trim();
404
+ }
405
+ function rawBaseForCommit(commit) {
406
+ return `https://raw.githubusercontent.com/extension-js/examples/${commit}`;
407
+ }
408
+ let releaseCache = null;
409
+ let releaseCacheExpiresAt = 0;
410
+ let releaseRequest = null;
411
+ function releaseForCommit(origin, commit) {
412
+ return {
413
+ commit,
414
+ metaUrl: `${origin}/templates/${commit}/templates-meta.json`,
415
+ filesBaseUrl: `${origin}/templates/${commit}/files`
416
+ };
417
+ }
418
+ async function resolveRelease() {
419
+ const now = Date.now();
420
+ if (releaseCache && now < releaseCacheExpiresAt) return releaseCache;
421
+ if (releaseRequest) return releaseRequest;
422
+ const origin = mediaOrigin();
423
+ const override = pinnedCommitOverride();
424
+ if (override) {
425
+ releaseCache = releaseForCommit(origin, override);
426
+ releaseCacheExpiresAt = now + CHANNEL_CACHE_TTL_MS;
427
+ return releaseCache;
428
+ }
429
+ releaseRequest = (async ()=>{
430
+ try {
431
+ const pointerUrl = `${origin}/templates/${channelName()}.json`;
432
+ const response = await fetch(pointerUrl, {
433
+ headers: {
434
+ Accept: "application/json"
435
+ }
436
+ });
437
+ if (!response.ok) return null;
438
+ const pointer = await response.json();
439
+ const commit = String(pointer?.commit || "").trim();
440
+ if (!commit) return null;
441
+ releaseCache = releaseForCommit(origin, commit);
442
+ releaseCacheExpiresAt = Date.now() + CHANNEL_CACHE_TTL_MS;
443
+ return releaseCache;
444
+ } catch {
445
+ return null;
446
+ }
447
+ })();
448
+ try {
449
+ return await releaseRequest;
450
+ } finally{
451
+ releaseRequest = null;
452
+ }
453
+ }
454
+ async function templateMetaUrls() {
455
+ const urls = [];
456
+ const release = await resolveRelease();
457
+ if (release) urls.push(release.metaUrl);
458
+ urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/templates-meta.json`);
459
+ return urls;
460
+ }
461
+ async function templateFileUrls(slug, relativePath) {
462
+ const urls = [];
463
+ const release = await resolveRelease();
464
+ if (release) urls.push(`${release.filesBaseUrl}/${slug}/${relativePath}`);
465
+ urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/examples/${slug}/${relativePath}`);
466
+ return urls;
467
+ }
391
468
  const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
392
469
  const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
393
470
  const CACHE_TTL_MS = 3600000;
394
- const TEMPLATES_META_URL = "https://github.com/extension-js/examples/releases/download/nightly/templates-meta.json";
395
471
  function isCacheValid() {
396
472
  try {
397
473
  const stat = node_fs.statSync(CACHE_FILE);
@@ -405,17 +481,22 @@ async function fetchTemplatesMeta() {
405
481
  const cached = JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
406
482
  return cached;
407
483
  }
408
- const response = await fetch(TEMPLATES_META_URL);
409
- if (!response.ok) {
410
- if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
411
- throw new Error(`Failed to fetch templates-meta.json: ${response.status}`);
412
- }
413
- const data = await response.json();
414
- node_fs.mkdirSync(CACHE_DIR, {
415
- recursive: true
416
- });
417
- node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
418
- return data;
484
+ let lastStatus = 0;
485
+ for (const url of (await templateMetaUrls()))try {
486
+ const response = await fetch(url);
487
+ if (!response.ok) {
488
+ lastStatus = response.status;
489
+ continue;
490
+ }
491
+ const data = await response.json();
492
+ node_fs.mkdirSync(CACHE_DIR, {
493
+ recursive: true
494
+ });
495
+ node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
496
+ return data;
497
+ } catch {}
498
+ if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
499
+ throw new Error(`Failed to fetch templates-meta.json (${lastStatus || "network error"}).`);
419
500
  }
420
501
  async function listTemplates(filters) {
421
502
  const meta = await fetchTemplatesMeta();
@@ -883,6 +964,7 @@ function isGeckoFamily(browser) {
883
964
  return GECKO_FAMILY.has(browser);
884
965
  }
885
966
  const CARRIER_DIR_NAME = "extension-dev-live-preview";
967
+ const CARRIER_EXTENSION_ID = "ibppeifnekhjjjmpjfiobccjlicbmgcb";
886
968
  const MARKER_FILE = "managed-by-extension-dev-mcp.json";
887
969
  function deriveCarrierId(source) {
888
970
  try {
@@ -1005,6 +1087,7 @@ function materializeCarrier(projectPath, browser) {
1005
1087
  "Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
1006
1088
  "Chromium-family only: Firefox has no externally_connectable channel for web pages."
1007
1089
  ],
1090
+ graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_inspect against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
1008
1091
  ...carrierId ? {
1009
1092
  bridgeProtocol: {
1010
1093
  carrierExtensionId: carrierId,
@@ -1751,6 +1834,7 @@ async function dev_handler(args) {
1751
1834
  child.on("exit", ()=>removeSession(args.projectPath, browser));
1752
1835
  await new Promise((resolve)=>setTimeout(resolve, 3000));
1753
1836
  const earlyOutput = spawned.readOutput();
1837
+ const cleanOutput = denoiseEarlyOutput(earlyOutput);
1754
1838
  if (null !== child.exitCode || null !== child.signalCode) {
1755
1839
  const code = child.exitCode;
1756
1840
  const signal = child.signalCode;
@@ -1763,12 +1847,12 @@ async function dev_handler(args) {
1763
1847
  exitCode: code,
1764
1848
  signal,
1765
1849
  error: `The dev server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running, so extension_logs/wait/eval and the control verbs have nothing to attach to.`,
1766
- output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
1850
+ output: cleanOutput.slice(0, 2000),
1767
1851
  logPath,
1768
1852
  hint: "Read `output` above for the cause: a port already in use, a manifest the build rejects, or a missing browser binary are the common ones. Fix it and call extension_dev again; extension_doctor with this projectPath will also report what the last session recorded."
1769
1853
  });
1770
1854
  }
1771
- const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(earlyOutput);
1855
+ const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(cleanOutput);
1772
1856
  if (compileFailed) return JSON.stringify({
1773
1857
  ok: false,
1774
1858
  status: "compile-failed",
@@ -1776,12 +1860,12 @@ async function dev_handler(args) {
1776
1860
  browser,
1777
1861
  pid,
1778
1862
  error: "The dev server started but the FIRST COMPILE FAILED, so the browser has nothing usable to load. The session is running; the extension is not.",
1779
- output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
1863
+ output: cleanOutput.slice(0, 2000),
1780
1864
  logPath,
1781
1865
  hint: "Fix the compile error in `output` above and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed."
1782
1866
  });
1783
1867
  const exitStamp = args.noBrowser ? null : browserExitStamp(args.projectPath, browser, spawnedAt);
1784
- const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(earlyOutput);
1868
+ const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
1785
1869
  if (exitStamp || profileLockHit) {
1786
1870
  const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
1787
1871
  return JSON.stringify({
@@ -1792,7 +1876,7 @@ async function dev_handler(args) {
1792
1876
  pid,
1793
1877
  ...exitStamp ?? {},
1794
1878
  error: `The dev server is running but the ${browser} browser it launched died during startup` + (profileLockHit ? " because its profile is locked by another browser instance." : "."),
1795
- output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
1879
+ output: cleanOutput.slice(0, 2000),
1796
1880
  logPath,
1797
1881
  hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`
1798
1882
  });
@@ -1840,7 +1924,7 @@ async function dev_handler(args) {
1840
1924
  } : {},
1841
1925
  capabilities,
1842
1926
  hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
1843
- earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500),
1927
+ earlyOutput: cleanOutput.slice(0, 500),
1844
1928
  logPath
1845
1929
  });
1846
1930
  }
@@ -1849,6 +1933,8 @@ function denoiseEarlyOutput(raw) {
1849
1933
  /^npm warn Unknown project config/i,
1850
1934
  /This will stop working in the next major version of npm/i,
1851
1935
  /^npm warn config/i,
1936
+ /^npm warn exec/i,
1937
+ /The following package(s)? (was|were) not found and will be installed/i,
1852
1938
  /V8: .*Invalid asm\.js/i,
1853
1939
  /^\(node:\d+\) V8:/i,
1854
1940
  /Use `node --trace-warnings/i,
@@ -2092,7 +2178,6 @@ async function preview_handler(args) {
2092
2178
  logPath
2093
2179
  });
2094
2180
  }
2095
- const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
2096
2181
  const get_template_source_schema = {
2097
2182
  name: "extension_get_template_source",
2098
2183
  description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
@@ -2140,14 +2225,17 @@ async function get_template_source_handler(args) {
2140
2225
  const fileContents = {};
2141
2226
  const errors = [];
2142
2227
  await Promise.all(args.files.map(async (filePath)=>{
2143
- const url = `${RAW_BASE}/${args.slug}/${filePath}`;
2144
- try {
2228
+ const urls = await templateFileUrls(args.slug, filePath);
2229
+ let lastStatus = 0;
2230
+ for (const url of urls)try {
2145
2231
  const response = await fetch(url);
2146
- if (response.ok) fileContents[filePath] = await response.text();
2147
- else errors.push(`${filePath}: ${response.status}`);
2148
- } catch (err) {
2149
- errors.push(`${filePath}: ${err instanceof Error ? err.message : "fetch failed"}`);
2150
- }
2232
+ if (response.ok) {
2233
+ fileContents[filePath] = await response.text();
2234
+ return;
2235
+ }
2236
+ lastStatus = response.status;
2237
+ } catch {}
2238
+ errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
2151
2239
  }));
2152
2240
  return JSON.stringify({
2153
2241
  ...meta,
@@ -5303,6 +5391,11 @@ const open_schema = {
5303
5391
  ]
5304
5392
  }
5305
5393
  };
5394
+ function sessionIsHeadless() {
5395
+ if (/^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "")) return true;
5396
+ return /(^|\s|=)-{1,2}headless\b/i.test(process.env.EXTENSION_BROWSER_FLAGS ?? "");
5397
+ }
5398
+ const HEADED_RELAUNCH = "start a headed session: extension_dev with replace: true, and in the environment set EXTENSION_HEADLESS=0 AND clear EXTENSION_BROWSER_FLAGS (it may carry --headless=new, which keeps the window hidden even with EXTENSION_HEADLESS=0)";
5306
5399
  async function open_handler(args) {
5307
5400
  const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
5308
5401
  if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
@@ -5347,7 +5440,7 @@ async function open_handler(args) {
5347
5440
  cli.push("--browser", browser);
5348
5441
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
5349
5442
  const raw = await runActVerb(cli, args.projectPath, args.timeout);
5350
- const headless = /^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "");
5443
+ const headless = sessionIsHeadless();
5351
5444
  if (headless && [
5352
5445
  "popup",
5353
5446
  "action",
@@ -5361,12 +5454,12 @@ async function open_handler(args) {
5361
5454
  try {
5362
5455
  const parsedFallback = JSON.parse(fallback);
5363
5456
  if (parsedFallback?.ok) {
5364
- parsedFallback.note = "The dev browser is headless (EXTENSION_HEADLESS=1), and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, start a headed session: extension_dev with replace: true, with EXTENSION_HEADLESS=0 (or unset) in the environment, then open the surface again without asTab.";
5457
+ parsedFallback.note = `The dev browser is headless, and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, ${HEADED_RELAUNCH}, then open the surface again without asTab.`;
5365
5458
  return JSON.stringify(parsedFallback);
5366
5459
  }
5367
5460
  } catch {}
5368
5461
  }
5369
- if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : "The dev browser is running headless (EXTENSION_HEADLESS=1), and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or start a headed session for the real window: extension_dev with replace: true, with EXTENSION_HEADLESS=0 (or unset) in the environment.";
5462
+ if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : `The dev browser is running headless, and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or for the real window, ${HEADED_RELAUNCH}.`;
5370
5463
  return JSON.stringify(parsed);
5371
5464
  }
5372
5465
  } catch {}
@@ -5752,11 +5845,78 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
5752
5845
  if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
5753
5846
  return creds;
5754
5847
  }
5755
- const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
5756
- const CONSOLE_BASE = "https://console.extension.dev";
5848
+ const PROD_ORIGINS = {
5849
+ www: "https://www.extension.dev",
5850
+ console: "https://console.extension.dev",
5851
+ inspect: "https://inspect.extension.dev",
5852
+ templates: "https://templates.extension.dev",
5853
+ intelligence: "https://intelligence.extension.dev",
5854
+ registry: "https://registry.extension.land",
5855
+ media: "https://media.extension.land"
5856
+ };
5857
+ const DEV_LOCALHOST_ORIGINS = {
5858
+ www: "http://localhost:3100",
5859
+ console: "http://console.extension.localhost",
5860
+ inspect: "http://inspect.extension.localhost",
5861
+ templates: "http://templates.extension.localhost",
5862
+ intelligence: "http://intelligence.extension.localhost",
5863
+ registry: "https://registry.extension.land",
5864
+ media: "https://media.extension.land"
5865
+ };
5866
+ function strip(value) {
5867
+ return String(value ?? "").trim().replace(/\/+$/, "");
5868
+ }
5869
+ function isLocalOrigin(url) {
5870
+ const raw = strip(url);
5871
+ if (!raw) return false;
5872
+ let host;
5873
+ try {
5874
+ host = new URL(raw).hostname;
5875
+ } catch {
5876
+ return false;
5877
+ }
5878
+ return "localhost" === host || "127.0.0.1" === host || "::1" === host || "[::1]" === host || "extension.localhost" === host || host.endsWith(".extension.localhost");
5879
+ }
5880
+ function resolveOrigins(overrides = {}, opts = {}) {
5881
+ const devLike = isLocalOrigin(overrides.www) || isLocalOrigin(overrides.console) || isLocalOrigin(opts.hint);
5882
+ const base = devLike ? DEV_LOCALHOST_ORIGINS : PROD_ORIGINS;
5883
+ const pick = (key)=>strip(overrides[key]) || base[key];
5884
+ return {
5885
+ www: pick("www"),
5886
+ console: pick("console"),
5887
+ inspect: pick("inspect"),
5888
+ templates: pick("templates"),
5889
+ intelligence: pick("intelligence"),
5890
+ registry: pick("registry"),
5891
+ media: pick("media")
5892
+ };
5893
+ }
5894
+ const seg = (value)=>encodeURIComponent(String(value));
5895
+ function urls_paths_join(base, sub) {
5896
+ if (!sub) return base;
5897
+ return `${base}/${sub.replace(/^\/+/, "")}`;
5898
+ }
5899
+ function consoleProjectPath(ref, page = "") {
5900
+ return urls_paths_join(`/${seg(ref.workspace)}/${seg(ref.project)}`, page);
5901
+ }
5902
+ PROD_ORIGINS.registry;
5903
+ function mcpOrigins(apiHint) {
5904
+ const www = String(apiHint || process.env.EXTENSION_DEV_API_URL || "").trim() || void 0;
5905
+ return resolveOrigins({
5906
+ www,
5907
+ console: process.env.EXTENSION_DEV_CONSOLE_URL,
5908
+ inspect: process.env.EXTENSION_DEV_INSPECT_URL,
5909
+ registry: process.env.EXTENSION_DEV_REGISTRY_URL,
5910
+ media: process.env.EXTENSION_MEDIA_ORIGIN
5911
+ }, {
5912
+ hint: www
5913
+ });
5914
+ }
5915
+ function consoleBase(apiHint) {
5916
+ return mcpOrigins(apiHint).console;
5917
+ }
5757
5918
  function registryBase() {
5758
- const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
5759
- return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
5919
+ return mcpOrigins().registry;
5760
5920
  }
5761
5921
  function resolveProjectRef(overrides) {
5762
5922
  const workspace = String(overrides?.workspace || "").trim();
@@ -5777,9 +5937,10 @@ function resolveProjectRef(overrides) {
5777
5937
  function registryFileUrl(ref, file) {
5778
5938
  return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
5779
5939
  }
5780
- function consoleProjectUrl(ref, page) {
5781
- if (!ref) return `${CONSOLE_BASE}`;
5782
- return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
5940
+ function consoleProjectUrl(ref, page, apiHint) {
5941
+ const base = consoleBase(apiHint);
5942
+ if (!ref) return base;
5943
+ return `${base}${consoleProjectPath(ref, page)}`;
5783
5944
  }
5784
5945
  async function fetchRegistryJson(url, fetchImpl = fetch) {
5785
5946
  let res;
@@ -5855,12 +6016,12 @@ function parseBuildIndex(json) {
5855
6016
  }
5856
6017
  return out;
5857
6018
  }
5858
- const DEFAULT_API = "https://www.extension.dev";
6019
+ const DEFAULT_API = PROD_ORIGINS.www;
5859
6020
  function tokenTtlNote(workspaceSlug, projectSlug) {
5860
6021
  const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
5861
6022
  workspace: workspaceSlug,
5862
6023
  project: projectSlug
5863
- }, "settings/access-tokens") : CONSOLE_BASE;
6024
+ }, "settings/access-tokens") : consoleBase();
5864
6025
  return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
5865
6026
  }
5866
6027
  function resolveApiBase(api) {
@@ -6101,7 +6262,6 @@ async function publish_handler(args) {
6101
6262
  }
6102
6263
  return JSON.stringify(data);
6103
6264
  }
6104
- const release_promote_DEFAULT_API = "https://www.extension.dev";
6105
6265
  const release_promote_schema = {
6106
6266
  name: "extension_release_promote",
6107
6267
  description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
@@ -6161,7 +6321,7 @@ async function release_promote_handler(args) {
6161
6321
  const buildId = String(args.buildId || "").trim();
6162
6322
  const channel = String(args.channel || "").trim();
6163
6323
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
6164
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
6324
+ const apiCheck = safeApiBase(resolveApiBase(args.api));
6165
6325
  if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
6166
6326
  const url = `${apiCheck.base}/api/cli/release/promote`;
6167
6327
  const body = {
@@ -6199,7 +6359,7 @@ async function release_promote_handler(args) {
6199
6359
  const enrich = {};
6200
6360
  const ref = resolveProjectRef();
6201
6361
  if (404 === res.status || "UNKNOWN_BUILD" === code) {
6202
- enrich.buildsPageUrl = consoleProjectUrl(ref, "builds");
6362
+ enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
6203
6363
  enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
6204
6364
  if (ref) {
6205
6365
  const channelsUrl = registryFileUrl(ref, "channels.json");
@@ -6299,7 +6459,6 @@ async function release_list_handler(args) {
6299
6459
  if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
6300
6460
  return JSON.stringify(result);
6301
6461
  }
6302
- const deploy_DEFAULT_API = "https://www.extension.dev";
6303
6462
  function storeMdWarnings(browsers, cwd) {
6304
6463
  const wantsFirefox = browsers.includes("firefox");
6305
6464
  const wantsEdge = browsers.includes("edge");
@@ -6389,7 +6548,7 @@ async function deploy_handler(args) {
6389
6548
  if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
6390
6549
  const buildSha = String(args.buildSha || "").trim();
6391
6550
  if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
6392
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || deploy_DEFAULT_API));
6551
+ const apiCheck = safeApiBase(resolveApiBase(args.api));
6393
6552
  if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
6394
6553
  const url = `${apiCheck.base}/api/cli/stores/submit`;
6395
6554
  const dryRun = false !== args.dryRun;
@@ -6434,7 +6593,7 @@ async function deploy_handler(args) {
6434
6593
  };
6435
6594
  if (dryRun) {
6436
6595
  const ref = resolveProjectRef();
6437
- const consoleStoresUrl = consoleProjectUrl(ref, "stores");
6596
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
6438
6597
  const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
6439
6598
  let health = null;
6440
6599
  let healthUnreadable = null;
@@ -6710,6 +6869,63 @@ async function store_status_handler(args) {
6710
6869
  if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
6711
6870
  return JSON.stringify(result);
6712
6871
  }
6872
+ const ENGINE_COMPANION_IDS = new Set([
6873
+ "kgdaecdpfkikjncaalnmmnjjfpofkcbl",
6874
+ CARRIER_EXTENSION_ID
6875
+ ]);
6876
+ const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
6877
+ async function verifyGuestLoaded(projectPath, browser, options) {
6878
+ let cdpPort;
6879
+ try {
6880
+ const resolved = await resolveCdpPort(projectPath, browser, {
6881
+ waitMs: options?.waitMs ?? 0
6882
+ });
6883
+ if (!resolved) return {
6884
+ checked: false,
6885
+ loaded: false,
6886
+ guestTargets: [],
6887
+ guestIds: [],
6888
+ reason: "No CDP port in the session's ready contract, so the browser's target list could not be queried (a headless Chromium session still exposes one; a gecko/Firefox session does not)."
6889
+ };
6890
+ cdpPort = resolved.port;
6891
+ const timeoutMs = options?.timeoutMs ?? 3000;
6892
+ const targets = await Promise.race([
6893
+ CDPClient.discoverTargets(cdpPort),
6894
+ new Promise((_, reject)=>setTimeout(()=>reject(new Error(`CDP /json timed out after ${timeoutMs}ms`)), timeoutMs))
6895
+ ]);
6896
+ const guestTargets = [];
6897
+ for (const t of targets){
6898
+ const match = EXTENSION_URL.exec(String(t.url ?? ""));
6899
+ if (!match) continue;
6900
+ const id = match[1].toLowerCase();
6901
+ if (!ENGINE_COMPANION_IDS.has(id)) guestTargets.push({
6902
+ id,
6903
+ type: String(t.type),
6904
+ url: String(t.url)
6905
+ });
6906
+ }
6907
+ const guestIds = [
6908
+ ...new Set(guestTargets.map((t)=>t.id))
6909
+ ];
6910
+ return {
6911
+ checked: true,
6912
+ loaded: guestTargets.length > 0,
6913
+ guestTargets,
6914
+ guestIds,
6915
+ cdpPort,
6916
+ reason: guestTargets.length > 0 ? `The browser lists ${guestIds.length} extension target${1 === guestIds.length ? "" : "s"} that are not the engine companion (${guestIds.join(", ")}), so the guest is loaded.` : "The browser's target list has no chrome-extension:// target other than the engine's devtools companion. On Chrome this is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it."
6917
+ };
6918
+ } catch (err) {
6919
+ return {
6920
+ checked: false,
6921
+ loaded: false,
6922
+ guestTargets: [],
6923
+ guestIds: [],
6924
+ cdpPort,
6925
+ reason: `Could not query the browser's target list${cdpPort ? ` on CDP port ${cdpPort}` : ""}: ${err?.message ?? String(err)}.`
6926
+ };
6927
+ }
6928
+ }
6713
6929
  function doctor_readReadyContract(projectPath, browser) {
6714
6930
  try {
6715
6931
  const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
@@ -6905,7 +7121,7 @@ function wait_isAlive(pid) {
6905
7121
  }
6906
7122
  const wait_schema = {
6907
7123
  name: "extension_wait",
6908
- description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with two separate facts: compiled (the compiler finished) and browserAttached (the extension's runtime connected from a live browser). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
7124
+ description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with these facts: compiled (the compiler finished), browserAttached (the engine's runtime executor connected), and for a browser session guestLoaded (the browser's OWN target list actually shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs (extension.js BUGS_TO_FIX §83); it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
6909
7125
  inputSchema: {
6910
7126
  type: "object",
6911
7127
  properties: {
@@ -6981,10 +7197,20 @@ async function wait_handler(args) {
6981
7197
  continue;
6982
7198
  }
6983
7199
  const runtimeErrors = recentErrorLogs(args.projectPath, browser, 3);
7200
+ const guestCheck = await verifyGuestLoaded(args.projectPath, browser);
7201
+ const warnings = [];
7202
+ if (runtimeErrors.length) warnings.push(`Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`);
7203
+ if (guestCheck.checked && !guestCheck.loaded) warnings.push("The engine reports the runtime attached, but the browser's own target list shows no chrome-extension:// target for your extension, only the engine companion. This is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it, and the control verbs will fail against a guest that is not there. Check the manifest and extension_logs.");
6984
7204
  return JSON.stringify({
6985
7205
  status: "ready",
6986
7206
  compiled: true,
6987
7207
  browserAttached: true,
7208
+ guestLoaded: guestCheck.checked ? guestCheck.loaded : null,
7209
+ ...guestCheck.checked ? {
7210
+ guestIds: guestCheck.guestIds
7211
+ } : {
7212
+ guestLoadNote: guestCheck.reason
7213
+ },
6988
7214
  command: contract.command,
6989
7215
  browser: contract.browser,
6990
7216
  port: contract.port,
@@ -6996,8 +7222,10 @@ async function wait_handler(args) {
6996
7222
  budgetMs,
6997
7223
  elapsedMs: Date.now() - start,
6998
7224
  ...runtimeErrors.length ? {
6999
- runtimeErrors,
7000
- warning: `Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`
7225
+ runtimeErrors
7226
+ } : {},
7227
+ ...warnings.length ? {
7228
+ warning: warnings.join(" ")
7001
7229
  } : {}
7002
7230
  });
7003
7231
  }
@@ -7696,7 +7924,10 @@ const logout_schema = {
7696
7924
  };
7697
7925
  async function logout_handler() {
7698
7926
  const creds = readCredentials();
7699
- const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? `https://console.extension.dev/${creds.workspaceSlug}/${creds.projectSlug}/settings/access-tokens` : null;
7927
+ const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
7928
+ workspace: creds.workspaceSlug,
7929
+ project: creds.projectSlug
7930
+ }, "settings/access-tokens") : null;
7700
7931
  const result = clearCredentials();
7701
7932
  return JSON.stringify({
7702
7933
  ok: true,
@@ -7,6 +7,7 @@
7
7
  * localhost) can then watch the session's real-lane trace and pair with it.
8
8
  */
9
9
  export declare const CARRIER_DIR_NAME = "extension-dev-live-preview";
10
+ export declare const CARRIER_EXTENSION_ID = "ibppeifnekhjjjmpjfiobccjlicbmgcb";
10
11
  /** Where the carrier lands inside a project. */
11
12
  export declare function carrierPath(projectPath: string): string;
12
13
  /** True only for a directory carrying our marker, i.e. ours to delete. */
@@ -49,6 +50,17 @@ export type CarrierMaterialization = {
49
50
  * was written down anywhere the caller would look.
50
51
  */
51
52
  limitations?: string[];
53
+ /**
54
+ * The graduation path, stated next to the shared-identity limitation so the
55
+ * caller is not left at a dead end. The carrier lane is the SHARED real lane
56
+ * (calls run as the carrier); but the guest is ALREADY loaded as itself in
57
+ * this same session, so its OWN storage, identity and messaging are reached by
58
+ * driving the guest directly with the control verbs, not the carrier bridge.
59
+ * This is the agent-lane twin of the inspect Trace tab's "load as your own
60
+ * extension" affordance: here the extension is already loaded, so graduating
61
+ * means targeting it, not launching it.
62
+ */
63
+ graduation?: string;
52
64
  /**
53
65
  * How to actually DRIVE the real lane. Seven personas reached a
54
66
  * permanently empty trace and concluded the feature was broken, because
@@ -0,0 +1,27 @@
1
+ export type GuestTarget = {
2
+ id: string;
3
+ type: string;
4
+ url: string;
5
+ };
6
+ export type GuestLoadCheck = {
7
+ checked: boolean;
8
+ loaded: boolean;
9
+ guestTargets: GuestTarget[];
10
+ guestIds: string[];
11
+ cdpPort?: number;
12
+ reason: string;
13
+ };
14
+ /**
15
+ * The BUGS_TO_FIX §83 oracle: ask the BROWSER whether the guest actually loaded,
16
+ * instead of trusting the engine's ready.json. Chrome refusing --load-extension
17
+ * is invisible to every engine surface (ready.json stamps status:ready, empty
18
+ * logs), so the only trustworthy "did the guest load" signal is a
19
+ * chrome-extension:// target in the browser's own CDP /json list. The engine's
20
+ * devtools companion is always present and is not the guest; a NON-companion
21
+ * extension target is. Never throws: an unreachable endpoint returns
22
+ * checked:false so callers can degrade instead of failing.
23
+ */
24
+ export declare function verifyGuestLoaded(projectPath: string, browser: string, options?: {
25
+ waitMs?: number;
26
+ timeoutMs?: number;
27
+ }): Promise<GuestLoadCheck>;
@@ -1,5 +1,17 @@
1
- export declare const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
2
- export declare const CONSOLE_BASE = "https://console.extension.dev";
1
+ import { type Origins } from "./urls-origins";
2
+ export declare const REGISTRY_BASE_DEFAULT: string;
3
+ /**
4
+ * Resolve the fleet origins from the MCP's env vars, once per call. Console,
5
+ * registry, and media all DERIVE from the platform base (`EXTENSION_DEV_API_URL`
6
+ * or a per-tool `api`): point that at a local stack and every dashboard link the
7
+ * MCP hands back follows to the dev host (`console.extension.localhost`) instead
8
+ * of silently pointing at prod. Explicit per-host env vars still win. Falls back
9
+ * to production when nothing is set. The dev-derivation lives in
10
+ * `@extensiondev/urls` so it stays identical to what the apps resolve.
11
+ */
12
+ export declare function mcpOrigins(apiHint?: string): Origins;
13
+ /** Base origin of the console dashboard, dev-aware (no trailing slash). */
14
+ export declare function consoleBase(apiHint?: string): string;
3
15
  export declare function registryBase(): string;
4
16
  export interface ProjectRef {
5
17
  workspace: string;
@@ -17,8 +29,14 @@ export declare function resolveProjectRef(overrides?: {
17
29
  }): ProjectRef | null;
18
30
  /** URL of a file under the project's `_extension-dev/` registry directory. */
19
31
  export declare function registryFileUrl(ref: ProjectRef, file: string): string;
20
- /** Console page URL for the project (builds, releases/new, stores, ...). */
21
- export declare function consoleProjectUrl(ref: ProjectRef | null, page: string): string;
32
+ /**
33
+ * Console page URL for the project (builds, releases/new, stores, ...). The
34
+ * origin is dev-aware; the path comes from the shared route contract so a
35
+ * console route rename is caught in one place. Pass `apiHint` (a tool's `api`
36
+ * arg) so the link matches that call's environment; otherwise it follows
37
+ * `EXTENSION_DEV_API_URL`/`EXTENSION_DEV_CONSOLE_URL`.
38
+ */
39
+ export declare function consoleProjectUrl(ref: ProjectRef | null, page: string, apiHint?: string): string;
22
40
  export type RegistryFetchResult<T> = {
23
41
  ok: true;
24
42
  json: T;
@@ -0,0 +1,2 @@
1
+ export declare function templateMetaUrls(): Promise<string[]>;
2
+ export declare function templateFileUrls(slug: string, relativePath: string): Promise<string[]>;
@@ -0,0 +1,34 @@
1
+ /** The set of app/service origins the fleet links between. */
2
+ export interface Origins {
3
+ www: string;
4
+ console: string;
5
+ inspect: string;
6
+ templates: string;
7
+ intelligence: string;
8
+ /** Public release-state JSON host (registry.extension.land). */
9
+ registry: string;
10
+ /** Content-addressed template corpus host (media.extension.land). */
11
+ media: string;
12
+ }
13
+ /** Production origins. The prod fallback for every resolver in the fleet. */
14
+ export declare const PROD_ORIGINS: Origins;
15
+ export declare const DEV_LOCALHOST_ORIGINS: Origins;
16
+ /**
17
+ * True for `localhost`, `127.0.0.1`, `[::1]`, or any `*.extension.localhost`
18
+ * host -- the shapes the dev proxy serves. Anything unparseable is treated as
19
+ * not-local so an odd value falls back to prod rather than leaking a dev host.
20
+ */
21
+ export declare function isLocalOrigin(url: string | undefined | null): boolean;
22
+ /**
23
+ * Resolve the full origin set from a caller's overrides.
24
+ *
25
+ * Precedence per origin: explicit override -> derived base (dev vs prod) ->
26
+ * prod. The base is chosen by whether the environment looks local: if any of
27
+ * `www`/`console`/`hint` points at a local host, unset origins derive from the
28
+ * Caddy dev map instead of prod. This is what lets an operator set only
29
+ * `EXTENSION_DEV_API_URL=http://localhost:3100` and still get a console link at
30
+ * `console.extension.localhost` rather than one that silently points at prod.
31
+ */
32
+ export declare function resolveOrigins(overrides?: Partial<Origins>, opts?: {
33
+ hint?: string;
34
+ }): Origins;
@@ -0,0 +1,56 @@
1
+ /** A project is addressed by its workspace slug + project slug across the fleet. */
2
+ export interface ProjectRef {
3
+ workspace: string;
4
+ project: string;
5
+ }
6
+ /** `/:workspace` (or a sub-page like `settings` / `settings/:section`). */
7
+ export declare function consoleWorkspacePath(workspace: string, page?: string): string;
8
+ /**
9
+ * `/:workspace/:project/<page>`. Pass a page tail from `ConsoleProjectPage`
10
+ * (e.g. `ConsoleProjectPage.builds`) rather than hand-typing it, so a route
11
+ * rename is a one-line change here instead of a scattered string edit.
12
+ */
13
+ export declare function consoleProjectPath(ref: ProjectRef, page?: string): string;
14
+ /**
15
+ * Named project-page tails. Values are the exact segments under
16
+ * `/:username/:reponame/` in console's route table; deep pages take ids.
17
+ */
18
+ export declare const ConsoleProjectPage: {
19
+ readonly overview: "";
20
+ readonly onboard: "onboard";
21
+ readonly activity: "activity";
22
+ readonly builds: "builds";
23
+ readonly build: (buildId: string, browser?: string) => string;
24
+ readonly releases: "releases";
25
+ readonly releasesNew: "releases/new";
26
+ readonly release: (releaseId: string) => string;
27
+ readonly stores: "stores";
28
+ readonly storesNew: "stores/new";
29
+ readonly store: (store: string) => string;
30
+ readonly storeSubmissions: (store: string) => string;
31
+ readonly storeSubmissionNew: (store: string) => string;
32
+ readonly storeSubmission: (store: string, submissionId: string) => string;
33
+ readonly projectSettings: "project-settings";
34
+ readonly projectSettingsSection: (section: string) => string;
35
+ /** Where CLI/MCP tokens are minted and revoked. */
36
+ readonly accessTokens: "settings/access-tokens";
37
+ };
38
+ export type QueryValue = string | number | boolean | null | undefined;
39
+ /**
40
+ * `/new?...` -- project creation. The public deep-link contract: external
41
+ * READMEs use `extension.dev/new?template=<slug>`, which www normalizes to
42
+ * `/import` preserving every param. Do not change the `template` key.
43
+ */
44
+ export declare function wwwNewPath(query?: Record<string, QueryValue>): string;
45
+ /** `/import?...` -- the ported import/deploy flow templates' Deploy buttons target. */
46
+ export declare function wwwImportPath(query?: Record<string, QueryValue>): string;
47
+ /** `/device` -- CLI/MCP device-code consent. Lives on www, never console. */
48
+ export declare function wwwDevicePath(): string;
49
+ /** `/templates` and `/templates/:slug` -- externally linkable gallery entry on www. */
50
+ export declare function wwwTemplatesPath(slug?: string): string;
51
+ export type TemplateTab = "preview" | "instructions" | "source";
52
+ /** `/:slug` for the default `preview` tab, else `/:slug/<tab>`. */
53
+ export declare function templateTabPath(slug: string, tab?: TemplateTab): string;
54
+ export type InspectTab = "preview" | "details" | "source" | "trace";
55
+ /** `/` for the default `preview` tab, else `/details` / `/source` / `/trace`. */
56
+ export declare function inspectTabPath(tab?: InspectTab): string;
@@ -44,6 +44,7 @@ export declare const schema: {
44
44
  required: string[];
45
45
  };
46
46
  };
47
+ export declare function sessionIsHeadless(): boolean;
47
48
  export declare function handler(args: ActArgs & {
48
49
  surface?: string;
49
50
  name?: string;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "5.7.0",
4
+ "version": "6.0.0",
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, 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
- "license": "MIT",
7
+ "license": "Apache-2.0",
8
8
  "author": {
9
9
  "name": "Cezar Augusto",
10
10
  "email": "hello@extension.dev",
@@ -42,7 +42,9 @@
42
42
  "server.json",
43
43
  "README.md",
44
44
  "CHANGELOG.md",
45
- "LICENSE"
45
+ "LICENSE",
46
+ "NOTICE",
47
+ "TRADEMARK.md"
46
48
  ],
47
49
  "scripts": {
48
50
  "clean": "rm -rf dist",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "5.7.0",
10
+ "version": "6.0.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.7.0",
16
+ "version": "6.0.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"