@extension.dev/mcp 10.10.4 → 10.10.5

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 50+ 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": "10.10.4",
13
+ "version": "10.10.5",
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 50+ 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": "10.10.4",
4
+ "version": "10.10.5",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 10.10.5
4
+
5
+ - `extension_build` and `extension_analyze` report store review risks
6
+ read off the built package, as `value.reviewRisks` plus one warning
7
+ each with the fix: access to every website, code the package did not
8
+ ship (`eval`, `new Function`, scripts or imports loaded from a URL), a
9
+ Firefox manifest without `data_collection_permissions`, and API
10
+ permissions no shipped script uses. They never block a build. On a
11
+ dev build only the manifest checks run, because the hot-reload runtime
12
+ and the injected `management` permission are not what a store
13
+ receives (measured on seven published extensions' dev builds against a
14
+ production build of an official template).
15
+
16
+ - The bundled `/extension-publish` command now takes an extension through
17
+ store review with this server's own tools instead of sending people to
18
+ the store consoles by hand: it validates, builds, checks for review
19
+ risks, rehearses with `extension_submit` (dry run), and submits only on
20
+ the user's yes through the approval link. It covers Chrome, Firefox,
21
+ Edge and Safari, and keeps whatever manifest version the project
22
+ declares rather than migrating it during a submission.
23
+
3
24
  ## 10.10.4
4
25
 
5
26
  - `--project <workspace>/<project>` (env `EXTENSION_DEV_PROJECT`) pins a
@@ -1,62 +1,39 @@
1
1
  ---
2
- description: Prepare an extension for store submission (Chrome Web Store, Firefox Add-ons)
3
- argument-hint: "[chrome|firefox|both]"
2
+ description: Get an extension through store review (Chrome Web Store, Firefox AMO, Edge Add-ons, App Store for Safari)
3
+ argument-hint: "[chrome|firefox|edge|safari|all]"
4
4
  ---
5
5
 
6
- Prepare the current extension for store submission. The user said: $ARGUMENTS
6
+ Take the current extension to store review. The user said: $ARGUMENTS
7
7
 
8
8
  ## Parse arguments
9
9
 
10
- Default to `both` (Chrome + Firefox). If the user specifies `chrome` or `firefox`, target only that store.
10
+ Default to `chrome` and `firefox`. `all` means chrome, firefox, edge and safari. Accept any comma-separated mix.
11
11
 
12
12
  ## Steps
13
13
 
14
- 1. **Validate the manifest**
15
- If MCP tool `extension_manifest_validate` is available, use it with the target browsers.
16
- Otherwise, read `src/manifest.json` and check:
17
- - Has `name`, `version`, `description`
18
- - Has appropriate `manifest_version` for each target
19
- - Permissions are minimal (no unnecessary permissions)
20
- - Has icons (at least 16x16, 48x48, 128x128)
21
-
22
- 2. **Build for each target browser**
23
-
24
- ```bash
25
- npx extension build --browser=chrome --zip
26
- npx extension build --browser=firefox --zip
27
- ```
28
-
29
- 3. **Inspect the builds**
30
- If MCP tool `extension_analyze` is available, use it for each browser build.
31
- Check:
32
- - Total size under 10MB (store limit)
33
- - No source maps in production build
34
- - Has manifest.json in dist
35
- - Has icons
36
-
37
- 4. **Report store readiness**
38
-
39
- ### Chrome Web Store
40
- - Zip location: `dist/chrome/<name>.zip`
41
- - Submit at: https://chrome.google.com/webstore/devconsole
42
- - Checklist:
43
- - [ ] manifest_version: 3
44
- - [ ] Icons: 128x128 PNG
45
- - [ ] Description under 132 characters (for listing)
46
- - [ ] Screenshots: 1280x800 or 640x400
47
- - [ ] Privacy policy URL (if using sensitive permissions)
48
-
49
- ### Firefox Add-ons (AMO)
50
- - Zip location: `dist/firefox/<name>.zip`
51
- - Submit at: https://addons.mozilla.org/developers/
52
- - Checklist:
53
- - [ ] manifest_version: 2 (recommended for broadest compat) or 3
54
- - [ ] No Chrome-only APIs without polyfill
55
- - [ ] Source code zip if using a bundler: `npx extension build --browser=firefox --zip --zip-source`
56
- - [ ] AMO requires source code review for minified/bundled code
57
-
58
- 5. **Flag issues** that would cause store rejection:
59
- - `<all_urls>` host permission without justification
60
- - `activeTab` + `scripting` without clear use case
61
- - Remote code loading (eval, Function constructor, remote scripts)
62
- - Excessive permissions for the extension's functionality
14
+ 1. **Check the manifest.** Call `extension_manifest_validate` for each target browser. Fix every `buildBlocking` finding before going on; report warnings.
15
+
16
+ 2. **Build store bundles.** Call `extension_build` with `zip: true` for each target. If it refuses on a blocking manifest error or returns compiler errors in `value.errors`, fix and rebuild.
17
+
18
+ 3. **Look for review risks.** Call `extension_analyze` on each build and flag anything a reviewer rejects:
19
+ - `<all_urls>` or broad host permissions without a reason in the listing
20
+ - remote code (`eval`, `new Function`, scripts loaded from a URL)
21
+ - permissions the code never uses
22
+ - Firefox: a missing `browser_specific_settings.gecko.data_collection_permissions` declaration, which AMO now requires for new add-ons and updates
23
+ - a bundle over 10 MB, source maps in the production build, missing 128px icon
24
+
25
+ 4. **Pick the build to submit.** Store review runs from a build on extension.dev, not from local files. Call `extension_release_status` with `include: "releases"` to find the sha. If the user has not shipped this version yet, say so and offer `extension_publish` first.
26
+
27
+ 5. **Rehearse.** Call `extension_submit` with the browsers and `buildSha`, leaving `dryRun` at its default (`true`). Show the per-store credential rows. A store whose credentials are not healthy is fixed in the extension.dev console (the response names the page); drop it from `browsers` or stop.
28
+
29
+ 6. **Submit only on a clear yes.** Ask the user to confirm the exact stores, build and channel. Then call `extension_submit` with `dryRun: false`.
30
+ - It answers `approval-required` with an `approvalUrl`: give the user that link. A workspace owner approves exactly this submission on extension.dev.
31
+ - When they say it is approved, call `extension_submit` again with the same arguments plus the returned `approvalId`. It runs once.
32
+
33
+ 7. **Report.** Call `extension_release_status` for the recorded outcome and review state per store, and give the user each store's listing or dashboard link from the response.
34
+
35
+ ## Rules
36
+
37
+ - Never submit without the user's confirmation in step 6, and never retry a real submission with a different build or store list under an old `approvalId`; request a new approval.
38
+ - Keep the manifest version the project already declares. Extension.js templates use `chromium:manifest_version: 3` and `firefox:manifest_version: 2`, and AMO accepts both; do not migrate a manifest as part of a submission.
39
+ - Store credentials are never arguments and never asked for in chat; they live in the extension.dev console.
package/dist/module.js CHANGED
@@ -236,7 +236,7 @@ __webpack_require__.d(workspace_create_namespaceObject, {
236
236
  handler: ()=>workspace_create_handler,
237
237
  schema: ()=>workspace_create_schema
238
238
  });
239
- var package_namespaceObject = JSON.parse('{"rE":"10.10.4","El":{"OP":"4.1.31"}}');
239
+ var package_namespaceObject = JSON.parse('{"rE":"10.10.5","El":{"OP":"4.1.31"}}');
240
240
  const ENVELOPE_SCHEMA = 1;
241
241
  const collectWarnings = (warnings)=>{
242
242
  if (!warnings) return [];
@@ -3769,6 +3769,229 @@ function readZipEntryNames(zipPath) {
3769
3769
  } catch {}
3770
3770
  }
3771
3771
  }
3772
+ const BROAD_PATTERNS = new Set([
3773
+ "<all_urls>",
3774
+ "*://*/*",
3775
+ "http://*/*",
3776
+ "https://*/*",
3777
+ "*://*/"
3778
+ ]);
3779
+ const API_PERMISSIONS = {
3780
+ alarms: [
3781
+ "alarms"
3782
+ ],
3783
+ bookmarks: [
3784
+ "bookmarks"
3785
+ ],
3786
+ contextMenus: [
3787
+ "contextMenus",
3788
+ "menus"
3789
+ ],
3790
+ menus: [
3791
+ "menus",
3792
+ "contextMenus"
3793
+ ],
3794
+ cookies: [
3795
+ "cookies"
3796
+ ],
3797
+ declarativeNetRequest: [
3798
+ "declarativeNetRequest"
3799
+ ],
3800
+ downloads: [
3801
+ "downloads"
3802
+ ],
3803
+ history: [
3804
+ "history"
3805
+ ],
3806
+ identity: [
3807
+ "identity"
3808
+ ],
3809
+ idle: [
3810
+ "idle"
3811
+ ],
3812
+ management: [
3813
+ "management"
3814
+ ],
3815
+ notifications: [
3816
+ "notifications"
3817
+ ],
3818
+ offscreen: [
3819
+ "offscreen"
3820
+ ],
3821
+ scripting: [
3822
+ "scripting"
3823
+ ],
3824
+ sidePanel: [
3825
+ "sidePanel"
3826
+ ],
3827
+ storage: [
3828
+ "storage"
3829
+ ],
3830
+ tabGroups: [
3831
+ "tabGroups"
3832
+ ],
3833
+ tabs: [
3834
+ "tabs"
3835
+ ],
3836
+ topSites: [
3837
+ "topSites"
3838
+ ],
3839
+ tts: [
3840
+ "tts"
3841
+ ],
3842
+ webNavigation: [
3843
+ "webNavigation"
3844
+ ],
3845
+ webRequest: [
3846
+ "webRequest"
3847
+ ]
3848
+ };
3849
+ const REMOTE_CODE_PATTERNS = [
3850
+ [
3851
+ "eval()",
3852
+ /\beval\s*\(/
3853
+ ],
3854
+ [
3855
+ "new Function()",
3856
+ /\bnew\s+Function\s*\(/
3857
+ ],
3858
+ [
3859
+ "importScripts from a URL",
3860
+ /importScripts\(\s*["'`]https?:/
3861
+ ],
3862
+ [
3863
+ "import() from a URL",
3864
+ /\bimport\(\s*["'`]https?:/
3865
+ ]
3866
+ ];
3867
+ const REMOTE_SCRIPT_TAG = /<script[^>]+src\s*=\s*["']https?:/i;
3868
+ const MAX_SCAN_BYTES = 8388608;
3869
+ function strings(value) {
3870
+ return Array.isArray(value) ? value.filter((v)=>"string" == typeof v) : [];
3871
+ }
3872
+ function hostPatterns(manifest) {
3873
+ const found = new Set();
3874
+ for (const pattern of strings(manifest.host_permissions))found.add(pattern);
3875
+ for (const perm of strings(manifest.permissions))if (perm.includes("://") || "<all_urls>" === perm) found.add(perm);
3876
+ const scripts = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
3877
+ for (const entry of scripts)for (const match of strings(entry?.matches))found.add(match);
3878
+ return [
3879
+ ...found
3880
+ ];
3881
+ }
3882
+ function readText(distPath, rel) {
3883
+ try {
3884
+ return node_fs.readFileSync(node_path.join(distPath, rel), "utf8");
3885
+ } catch {
3886
+ return "";
3887
+ }
3888
+ }
3889
+ function reviewRisks(input) {
3890
+ const risks = [];
3891
+ const development = input.development ?? isDevelopmentBuild(input.files);
3892
+ const broad = hostPatterns(input.manifest).filter((p)=>BROAD_PATTERNS.has(p));
3893
+ if (broad.length) risks.push({
3894
+ code: "BROAD_HOST_ACCESS",
3895
+ message: `The extension asks for access to every website (${broad.join(", ")}). Chrome Web Store and AMO reviewers ask why, and the review takes longer.`,
3896
+ fix: "List only the sites the extension works on, or use activeTab so access is granted per click; if every site is the point, say why in the listing.",
3897
+ permissions: broad
3898
+ });
3899
+ const scripts = input.files.filter((f)=>/\.(m?js|cjs)$/.test(f.path) && !f.path.endsWith(".map"));
3900
+ const pages = input.files.filter((f)=>/\.html?$/.test(f.path));
3901
+ const hits = new Map();
3902
+ const note = (label, file)=>{
3903
+ if (!hits.has(label)) hits.set(label, new Set());
3904
+ hits.get(label).add(file);
3905
+ };
3906
+ let scanned = 0;
3907
+ let source = "";
3908
+ for (const file of scripts){
3909
+ if (scanned >= MAX_SCAN_BYTES) break;
3910
+ const text = readText(input.distPath, file.path);
3911
+ scanned += text.length;
3912
+ source += `\n${text}`;
3913
+ for (const [label, pattern] of REMOTE_CODE_PATTERNS)if (pattern.test(text)) note(label, file.path);
3914
+ }
3915
+ for (const file of pages)if (REMOTE_SCRIPT_TAG.test(readText(input.distPath, file.path))) note("a <script> loaded from a URL", file.path);
3916
+ if (hits.size && !development) {
3917
+ const labels = [
3918
+ ...hits.keys()
3919
+ ];
3920
+ risks.push({
3921
+ code: "REMOTE_CODE",
3922
+ message: `The package contains ${labels.join(", ")}. Manifest V3 stores reject extensions that run code they did not ship, and reviewers flag these patterns even inside a bundled library.`,
3923
+ fix: "Remove the call, or confirm it only ever runs code that ships in the package and be ready to say so in review.",
3924
+ files: [
3925
+ ...new Set([
3926
+ ...hits.values()
3927
+ ].flatMap((set)=>[
3928
+ ...set
3929
+ ]))
3930
+ ].slice(0, 10)
3931
+ });
3932
+ }
3933
+ if (isGeckoFamily(input.browser)) {
3934
+ const gecko = input.manifest.browser_specific_settings?.gecko;
3935
+ if (!gecko || !("data_collection_permissions" in gecko)) risks.push({
3936
+ code: "FIREFOX_DATA_COLLECTION_MISSING",
3937
+ message: "The Firefox manifest has no browser_specific_settings.gecko.data_collection_permissions. AMO requires it for new add-ons and for updates.",
3938
+ fix: 'Add "browser_specific_settings": { "gecko": { "data_collection_permissions": { "required": ["none"] } } }, or list what the extension does collect.'
3939
+ });
3940
+ }
3941
+ if (source && scanned < MAX_SCAN_BYTES && !development) {
3942
+ const unused = strings(input.manifest.permissions).filter((perm)=>{
3943
+ const apis = API_PERMISSIONS[perm];
3944
+ if (!apis) return false;
3945
+ return !apis.some((api)=>new RegExp(`\\.${api}\\b`).test(source));
3946
+ });
3947
+ if (unused.length) risks.push({
3948
+ code: "UNUSED_PERMISSION",
3949
+ message: `The manifest asks for ${unused.join(", ")}, but no shipped script uses ${1 === unused.length ? "that API" : "those APIs"}. Reviewers reject permissions an extension does not need.`,
3950
+ fix: `Remove ${unused.join(", ")} from permissions, or keep ${1 === unused.length ? "it" : "them"} if the call happens in code this scan cannot see.`,
3951
+ permissions: unused
3952
+ });
3953
+ }
3954
+ return risks;
3955
+ }
3956
+ function isDevelopmentBuild(files) {
3957
+ return files.some((f)=>f.path.endsWith(".map") || /hot-update\./.test(f.path));
3958
+ }
3959
+ function reviewRiskWarnings(risks) {
3960
+ return risks.map((risk)=>`Store review: ${risk.message} ${risk.fix}`);
3961
+ }
3962
+ function listFiles(dir, base = "") {
3963
+ let entries;
3964
+ try {
3965
+ entries = node_fs.readdirSync(dir, {
3966
+ withFileTypes: true
3967
+ });
3968
+ } catch {
3969
+ return [];
3970
+ }
3971
+ const out = [];
3972
+ for (const entry of entries){
3973
+ const rel = base ? `${base}/${entry.name}` : entry.name;
3974
+ if (entry.isDirectory()) out.push(...listFiles(node_path.join(dir, entry.name), rel));
3975
+ else out.push({
3976
+ path: rel
3977
+ });
3978
+ }
3979
+ return out;
3980
+ }
3981
+ function reviewDist(distPath, browser) {
3982
+ let manifest;
3983
+ try {
3984
+ manifest = JSON.parse(node_fs.readFileSync(node_path.join(distPath, "manifest.json"), "utf8"));
3985
+ } catch {
3986
+ return [];
3987
+ }
3988
+ return reviewRisks({
3989
+ distPath,
3990
+ browser,
3991
+ manifest,
3992
+ files: listFiles(distPath)
3993
+ });
3994
+ }
3772
3995
  const build_COMMAND = "extension_build";
3773
3996
  function readBuildSummary(projectPath, browser, since) {
3774
3997
  const file = session_paths_buildSummaryPath(projectPath, browser);
@@ -4210,6 +4433,7 @@ async function build_handler(args) {
4210
4433
  const buildWarningsTruncated = buildWarnings.length && "number" == typeof engineSummary?.warnings_count && engineSummary.warnings_count > buildWarnings.length ? engineSummary.warnings_count : void 0;
4211
4434
  const distDir = node_path.resolve(args.projectPath, "dist", browser);
4212
4435
  const entrypoints = builtEntrypoints(distDir);
4436
+ const risks = reviewDist(distDir, browser);
4213
4437
  const contamination = carrierContamination(node_path.resolve(args.projectPath, "dist"));
4214
4438
  const uncheckedNote = contamination.unchecked.length ? `Could not read the entry table of ${contamination.unchecked.join(", ")}, so those archives were not checked for the live-preview carrier. Unpack and check them yourself before submitting.` : null;
4215
4439
  if (contamination.paths.length) return envelope_envelope({
@@ -4293,6 +4517,9 @@ async function build_handler(args) {
4293
4517
  ...entrypoints.length ? {
4294
4518
  entrypoints
4295
4519
  } : {},
4520
+ ...risks.length ? {
4521
+ reviewRisks: risks
4522
+ } : {},
4296
4523
  ...void 0 !== buildWarningsTruncated ? {
4297
4524
  buildWarningsTruncated
4298
4525
  } : {},
@@ -4313,6 +4540,7 @@ async function build_handler(args) {
4313
4540
  ...warnings,
4314
4541
  ...preflight?.warnings ?? [],
4315
4542
  ...buildWarnings,
4543
+ ...reviewRiskWarnings(risks),
4316
4544
  ...zipNotes,
4317
4545
  uncheckedNote,
4318
4546
  derivedBundleIdNote,
@@ -13069,9 +13297,17 @@ async function analyze_handler(args) {
13069
13297
  }
13070
13298
  const note = "development" === buildType ? `This dist contains ${formatBytes(sourcemapSize)} of sourcemaps and looks like a dev build; run extension_build for production sizes. shippableSize excludes sourcemaps.` : void 0;
13071
13299
  const archiveNote = archiveSize > 0 ? `This dist contains ${formatBytes(archiveSize)} of .zip archive(s) (store packaging output, written into dist by zip builds). shippableSize excludes them so the packaged copy does not double-count the files it contains.` : void 0;
13300
+ const risks = reviewRisks({
13301
+ distPath,
13302
+ browser,
13303
+ manifest,
13304
+ files,
13305
+ development: "development" === buildType
13306
+ });
13072
13307
  const result = {
13073
13308
  browser,
13074
13309
  distPath,
13310
+ reviewRisks: risks,
13075
13311
  entrypoints,
13076
13312
  buildType,
13077
13313
  totalSize,
@@ -13120,6 +13356,7 @@ async function analyze_handler(args) {
13120
13356
  status: "analyzed",
13121
13357
  value: result,
13122
13358
  warnings: [
13359
+ ...reviewRiskWarnings(risks),
13123
13360
  ...sizeWarnings,
13124
13361
  note,
13125
13362
  archiveNote
@@ -0,0 +1,23 @@
1
+ export type ReviewRiskCode = "BROAD_HOST_ACCESS" | "REMOTE_CODE" | "FIREFOX_DATA_COLLECTION_MISSING" | "UNUSED_PERMISSION";
2
+ export interface ReviewRisk {
3
+ code: ReviewRiskCode;
4
+ message: string;
5
+ fix: string;
6
+ files?: string[];
7
+ permissions?: string[];
8
+ }
9
+ export declare function reviewRisks(input: {
10
+ distPath: string;
11
+ browser: string;
12
+ manifest: Record<string, unknown>;
13
+ files: Array<{
14
+ path: string;
15
+ type?: string;
16
+ }>;
17
+ development?: boolean;
18
+ }): ReviewRisk[];
19
+ export declare function isDevelopmentBuild(files: Array<{
20
+ path: string;
21
+ }>): boolean;
22
+ export declare function reviewRiskWarnings(risks: ReviewRisk[]): string[];
23
+ export declare function reviewDist(distPath: string, browser: string): ReviewRisk[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "10.10.4",
4
+ "version": "10.10.5",
5
5
  "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 31 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
6
6
  "mcpName": "io.github.extensiondev/mcp",
7
7
  "license": "Apache-2.0",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "10.10.4",
10
+ "version": "10.10.5",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@extension.dev/mcp",
16
- "version": "10.10.4",
16
+ "version": "10.10.5",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"