@extension.dev/mcp 10.10.3 → 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.3",
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.3",
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,43 @@
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
+
24
+ ## 10.10.4
25
+
26
+ - `--project <workspace>/<project>` (env `EXTENSION_DEV_PROJECT`) pins a
27
+ server to one project. Every unnamed credential read takes that
28
+ project's login instead of the most recent one, an agent signed in to
29
+ several projects can no longer act on the wrong one, and a call that
30
+ names a different project is refused with `status: "project-pinned"`.
31
+ A pinned project with no stored login falls back to
32
+ `EXTENSION_DEV_TOKEN`, so CI keeps working.
33
+ - New `@extension.dev/mcp/clients` export: `CLIENTS` and
34
+ `buildRecipe({ client, reach, strictApproval, project })` build the
35
+ exact setup for Claude Code, Cursor, VS Code, Codex and any
36
+ `mcpServers` client. The README's Setup section is generated from it
37
+ (`pnpm readme:clients`, checked by a spec), and the extension.dev
38
+ console's Connect dialog renders the same recipes, so the instructions
39
+ cannot drift. It never emits a setting that turns approvals off.
40
+
3
41
  ## 10.10.3
4
42
 
5
43
  - The engine pin moves off the canary to Extension.js `4.1.31` stable:
package/README.md CHANGED
@@ -45,10 +45,12 @@ Built on [Extension.js](https://extension.js.org), the open-source cross-browser
45
45
 
46
46
  ## Setup
47
47
 
48
+ <!-- setup:start (generated from src/clients, run pnpm readme:clients) -->
49
+
48
50
  ### Claude Code
49
51
 
50
52
  ```bash
51
- claude mcp add extension-dev npx @extension.dev/mcp
53
+ claude mcp add extension-dev -- npx @extension.dev/mcp
52
54
  ```
53
55
 
54
56
  Or install it as a plugin, the MCP server plus the `/extension`, `/extension-add`, `/extension-debug`, and `/extension-publish` commands in one step:
@@ -62,19 +64,84 @@ Or install it as a plugin, the MCP server plus the `/extension`, `/extension-add
62
64
 
63
65
  [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=extension-dev&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAZXh0ZW5zaW9uLmRldi9tY3AiXX0%3D)
64
66
 
65
- ### Claude Desktop / `.mcp.json`
67
+ `.cursor/mcp.json`:
66
68
 
67
69
  ```json
68
70
  {
69
71
  "mcpServers": {
70
72
  "extension-dev": {
71
73
  "command": "npx",
72
- "args": ["@extension.dev/mcp"]
74
+ "args": [
75
+ "@extension.dev/mcp"
76
+ ]
73
77
  }
74
78
  }
75
79
  }
76
80
  ```
77
81
 
82
+ ### VS Code (Also GitHub Copilot)
83
+
84
+ ```bash
85
+ code --add-mcp '{"name":"extension-dev","command":"npx","args":["@extension.dev/mcp"]}'
86
+ ```
87
+
88
+ `.vscode/mcp.json`:
89
+
90
+ ```json
91
+ {
92
+ "servers": {
93
+ "extension-dev": {
94
+ "type": "stdio",
95
+ "command": "npx",
96
+ "args": [
97
+ "@extension.dev/mcp"
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### Codex
105
+
106
+ ```bash
107
+ codex mcp add extension-dev -- npx @extension.dev/mcp
108
+ ```
109
+
110
+ `~/.codex/config.toml`:
111
+
112
+ ```toml
113
+ [mcp_servers.extension-dev]
114
+ command = "npx"
115
+ args = ["@extension.dev/mcp"]
116
+ ```
117
+
118
+ ### Other clients (Claude Desktop and .mcp.json)
119
+
120
+ `.mcp.json`:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "extension-dev": {
126
+ "command": "npx",
127
+ "args": [
128
+ "@extension.dev/mcp"
129
+ ]
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Signing in and pinning a project
136
+
137
+ The platform tools need a login. Sign in once per project, then add `--project <workspace>/<project>` to the server's arguments (or set `EXTENSION_DEV_PROJECT`) so that server only ever acts on that project, however many logins this machine holds. The console's Connect dialog fills both in for your project.
138
+
139
+ ```bash
140
+ npx @extension.dev/mcp login --project <workspace>/<project>
141
+ ```
142
+
143
+ <!-- setup:end -->
144
+
78
145
  ### Choosing what the agent can reach
79
146
 
80
147
  Two flags (or environment variables) narrow the server before an agent sees it:
@@ -27,6 +27,7 @@ Server flags:
27
27
  --features=local,platform Tool groups to expose (default: both)
28
28
  --no-ship Refuse publish, promote, real submits,
29
29
  share links and share revokes
30
+ --project <workspace>/<proj> Use only this project's login
30
31
  `
31
32
 
32
33
  const [, , cmd, ...rest] = process.argv
@@ -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.
@@ -0,0 +1,157 @@
1
+ const CLIENTS = [
2
+ {
3
+ id: "claude-code",
4
+ label: "Claude Code"
5
+ },
6
+ {
7
+ id: "cursor",
8
+ label: "Cursor"
9
+ },
10
+ {
11
+ id: "vscode",
12
+ label: "VS Code",
13
+ subtitle: "Also GitHub Copilot"
14
+ },
15
+ {
16
+ id: "codex",
17
+ label: "Codex"
18
+ },
19
+ {
20
+ id: "json",
21
+ label: "Other clients",
22
+ subtitle: "Claude Desktop and .mcp.json"
23
+ }
24
+ ];
25
+ const SERVER_NAME = "extension-dev";
26
+ const PACKAGE = "@extension.dev/mcp";
27
+ function serverArgs(input) {
28
+ const args = [
29
+ PACKAGE
30
+ ];
31
+ if ("local" !== input.reach && input.project) args.push("--project", input.project);
32
+ if ("no-ship" === input.reach) args.push("--no-ship");
33
+ if ("local" === input.reach) args.push("--features=local");
34
+ return args;
35
+ }
36
+ function serverEnv(input) {
37
+ return "everything" === input.reach && input.strictApproval ? {
38
+ EXTENSION_DEV_APPROVAL_GATE: "1"
39
+ } : {};
40
+ }
41
+ const shellQuote = (value)=>/^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
42
+ const base64 = (text)=>"function" == typeof btoa ? btoa(text) : globalThis.Buffer.from(text).toString("base64");
43
+ const tomlString = (value)=>JSON.stringify(value);
44
+ function loginCommand(project) {
45
+ return `npx ${PACKAGE} login --project ${shellQuote(project)}`;
46
+ }
47
+ function buildRecipe(input) {
48
+ const args = serverArgs(input);
49
+ const env = serverEnv(input);
50
+ const envEntries = Object.entries(env);
51
+ const stdio = {
52
+ command: "npx",
53
+ args,
54
+ ...envEntries.length ? {
55
+ env
56
+ } : {}
57
+ };
58
+ const login = loginCommand(input.project);
59
+ const argLine = args.map(shellQuote).join(" ");
60
+ switch(input.client){
61
+ case "claude-code":
62
+ return {
63
+ client: "claude-code",
64
+ command: [
65
+ "claude mcp add",
66
+ SERVER_NAME,
67
+ ...envEntries.map(([k, v])=>`-e ${k}=${shellQuote(v)}`),
68
+ "-- npx",
69
+ argLine
70
+ ].join(" "),
71
+ login
72
+ };
73
+ case "cursor":
74
+ return {
75
+ client: "cursor",
76
+ deeplink: `https://cursor.com/en/install-mcp?name=${SERVER_NAME}&config=${encodeURIComponent(base64(JSON.stringify(stdio)))}`,
77
+ config: {
78
+ language: "json",
79
+ path: ".cursor/mcp.json",
80
+ text: JSON.stringify({
81
+ mcpServers: {
82
+ [SERVER_NAME]: stdio
83
+ }
84
+ }, null, 2)
85
+ },
86
+ login
87
+ };
88
+ case "vscode":
89
+ {
90
+ const named = {
91
+ name: SERVER_NAME,
92
+ ...stdio
93
+ };
94
+ return {
95
+ client: "vscode",
96
+ command: `code --add-mcp ${shellQuote(JSON.stringify(named))}`,
97
+ deeplink: `vscode:mcp/install?${encodeURIComponent(JSON.stringify(named))}`,
98
+ config: {
99
+ language: "json",
100
+ path: ".vscode/mcp.json",
101
+ text: JSON.stringify({
102
+ servers: {
103
+ [SERVER_NAME]: {
104
+ type: "stdio",
105
+ ...stdio
106
+ }
107
+ }
108
+ }, null, 2)
109
+ },
110
+ login
111
+ };
112
+ }
113
+ case "codex":
114
+ {
115
+ const lines = [
116
+ `[mcp_servers.${SERVER_NAME}]`,
117
+ 'command = "npx"',
118
+ `args = [${args.map(tomlString).join(", ")}]`
119
+ ];
120
+ if (envEntries.length) {
121
+ lines.push("", `[mcp_servers.${SERVER_NAME}.env]`);
122
+ for (const [k, v] of envEntries)lines.push(`${k} = ${tomlString(v)}`);
123
+ }
124
+ return {
125
+ client: "codex",
126
+ command: [
127
+ "codex mcp add",
128
+ SERVER_NAME,
129
+ ...envEntries.map(([k, v])=>`--env ${k}=${shellQuote(v)}`),
130
+ "-- npx",
131
+ argLine
132
+ ].join(" "),
133
+ config: {
134
+ language: "toml",
135
+ path: "~/.codex/config.toml",
136
+ text: lines.join("\n")
137
+ },
138
+ login
139
+ };
140
+ }
141
+ case "json":
142
+ return {
143
+ client: "json",
144
+ config: {
145
+ language: "json",
146
+ path: ".mcp.json",
147
+ text: JSON.stringify({
148
+ mcpServers: {
149
+ [SERVER_NAME]: stdio
150
+ }
151
+ }, null, 2)
152
+ },
153
+ login
154
+ };
155
+ }
156
+ }
157
+ export { CLIENTS, PACKAGE, SERVER_NAME, buildRecipe, loginCommand, serverArgs, serverEnv };
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.3","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 [];
@@ -509,8 +509,20 @@ function readCredentialStore() {
509
509
  entries
510
510
  };
511
511
  }
512
+ const PROJECT_PIN_ENV = "EXTENSION_DEV_PROJECT";
513
+ function pinnedProject() {
514
+ return String(process.env[PROJECT_PIN_ENV] || "").trim().toLowerCase();
515
+ }
516
+ function isProjectRef(value) {
517
+ return /^[a-z0-9][a-z0-9-_.]*\/[a-z0-9][a-z0-9-_.]*$/i.test(value);
518
+ }
519
+ function sameProject(named, pinned) {
520
+ const a = named.trim().toLowerCase();
521
+ if (!a || !pinned) return true;
522
+ return a.includes("/") ? a === pinned : a === pinned.split("/")[1];
523
+ }
512
524
  function selectEntry(store, selector) {
513
- const wanted = String(selector?.project ?? "").trim().toLowerCase();
525
+ const wanted = (String(selector?.project ?? "").trim() || pinnedProject()).toLowerCase();
514
526
  if (!wanted) return store.active && store.entries[store.active] || Object.values(store.entries)[0] || null;
515
527
  if (wanted.includes("/")) return store.entries[wanted] ?? null;
516
528
  const bySlug = Object.values(store.entries).filter((entry)=>entry.projectSlug.toLowerCase() === wanted);
@@ -3757,6 +3769,229 @@ function readZipEntryNames(zipPath) {
3757
3769
  } catch {}
3758
3770
  }
3759
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
+ }
3760
3995
  const build_COMMAND = "extension_build";
3761
3996
  function readBuildSummary(projectPath, browser, since) {
3762
3997
  const file = session_paths_buildSummaryPath(projectPath, browser);
@@ -4198,6 +4433,7 @@ async function build_handler(args) {
4198
4433
  const buildWarningsTruncated = buildWarnings.length && "number" == typeof engineSummary?.warnings_count && engineSummary.warnings_count > buildWarnings.length ? engineSummary.warnings_count : void 0;
4199
4434
  const distDir = node_path.resolve(args.projectPath, "dist", browser);
4200
4435
  const entrypoints = builtEntrypoints(distDir);
4436
+ const risks = reviewDist(distDir, browser);
4201
4437
  const contamination = carrierContamination(node_path.resolve(args.projectPath, "dist"));
4202
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;
4203
4439
  if (contamination.paths.length) return envelope_envelope({
@@ -4281,6 +4517,9 @@ async function build_handler(args) {
4281
4517
  ...entrypoints.length ? {
4282
4518
  entrypoints
4283
4519
  } : {},
4520
+ ...risks.length ? {
4521
+ reviewRisks: risks
4522
+ } : {},
4284
4523
  ...void 0 !== buildWarningsTruncated ? {
4285
4524
  buildWarningsTruncated
4286
4525
  } : {},
@@ -4301,6 +4540,7 @@ async function build_handler(args) {
4301
4540
  ...warnings,
4302
4541
  ...preflight?.warnings ?? [],
4303
4542
  ...buildWarnings,
4543
+ ...reviewRiskWarnings(risks),
4304
4544
  ...zipNotes,
4305
4545
  uncheckedNote,
4306
4546
  derivedBundleIdNote,
@@ -7921,12 +8161,15 @@ async function openSidebarThroughGesture(projectPath, browser, refusal) {
7921
8161
  return actFrameJson(refusal);
7922
8162
  }
7923
8163
  function resolveToken(options = {}) {
7924
- const project = String(options.project ?? "").trim();
8164
+ const named = String(options.project ?? "").trim();
8165
+ const project = named || pinnedProject();
7925
8166
  if (project) {
7926
8167
  const creds = readValidCredentials(void 0, {
7927
8168
  project
7928
8169
  });
7929
- return creds?.token ? String(creds.token).trim() : "";
8170
+ if (creds?.token) return String(creds.token).trim();
8171
+ if (named) return "";
8172
+ return String(process.env.EXTENSION_DEV_TOKEN || "").trim();
7930
8173
  }
7931
8174
  const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
7932
8175
  if (fromEnv) return fromEnv;
@@ -13054,9 +13297,17 @@ async function analyze_handler(args) {
13054
13297
  }
13055
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;
13056
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
+ });
13057
13307
  const result = {
13058
13308
  browser,
13059
13309
  distPath,
13310
+ reviewRisks: risks,
13060
13311
  entrypoints,
13061
13312
  buildType,
13062
13313
  totalSize,
@@ -13105,6 +13356,7 @@ async function analyze_handler(args) {
13105
13356
  status: "analyzed",
13106
13357
  value: result,
13107
13358
  warnings: [
13359
+ ...reviewRiskWarnings(risks),
13108
13360
  ...sizeWarnings,
13109
13361
  note,
13110
13362
  archiveNote
@@ -18058,30 +18310,41 @@ const truthy = (raw)=>{
18058
18310
  return "" !== value && "0" !== value && "false" !== value && "off" !== value;
18059
18311
  };
18060
18312
  function isServerFlag(arg) {
18061
- return "--no-ship" === arg || "--features" === arg || arg.startsWith("--features=");
18313
+ return "--no-ship" === arg || "--features" === arg || arg.startsWith("--features=") || "--project" === arg || arg.startsWith("--project=");
18062
18314
  }
18063
18315
  function resolveServerOptions(argv, env) {
18064
18316
  let rawFeatures = env[FEATURES_ENV];
18065
18317
  let noShip = truthy(env[NO_SHIP_ENV]);
18318
+ let project = String(env[PROJECT_PIN_ENV] || "").trim().toLowerCase();
18066
18319
  for(let i = 0; i < argv.length; i++){
18067
18320
  const arg = argv[i];
18068
18321
  if ("--no-ship" === arg) noShip = true;
18069
18322
  else if ("--features" === arg) rawFeatures = argv[++i] ?? "";
18323
+ else if (arg.startsWith("--features=")) rawFeatures = arg.slice(11);
18324
+ else if ("--project" === arg) project = String(argv[++i] ?? "").trim().toLowerCase();
18070
18325
  else {
18071
- if (!arg.startsWith("--features=")) return {
18326
+ if (!arg.startsWith("--project=")) return {
18072
18327
  ok: false,
18073
18328
  message: `Unknown flag "${arg}".`
18074
18329
  };
18075
- rawFeatures = arg.slice(11);
18330
+ project = arg.slice(10).trim().toLowerCase();
18076
18331
  }
18077
18332
  }
18333
+ if (project && !isProjectRef(project)) return {
18334
+ ok: false,
18335
+ message: `--project takes "<workspace>/<project>", got "${project}".`
18336
+ };
18337
+ const pin = project ? {
18338
+ project
18339
+ } : {};
18078
18340
  if (void 0 === rawFeatures || "" === rawFeatures.trim()) return {
18079
18341
  ok: true,
18080
18342
  options: {
18081
18343
  features: [
18082
18344
  ...FEATURE_GROUPS
18083
18345
  ],
18084
- noShip
18346
+ noShip,
18347
+ ...pin
18085
18348
  }
18086
18349
  };
18087
18350
  const names = rawFeatures.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);
@@ -18095,7 +18358,8 @@ function resolveServerOptions(argv, env) {
18095
18358
  ok: true,
18096
18359
  options: {
18097
18360
  features,
18098
- noShip
18361
+ noShip,
18362
+ ...pin
18099
18363
  }
18100
18364
  };
18101
18365
  }
@@ -18138,6 +18402,40 @@ function disabledToolEnvelope(name, args, options) {
18138
18402
  });
18139
18403
  return null;
18140
18404
  }
18405
+ function pinProjectArgs(name, args, inputSchema, options) {
18406
+ const pinned = options.project;
18407
+ if (!pinned) return {
18408
+ args
18409
+ };
18410
+ const named = "string" == typeof args.project ? args.project : "";
18411
+ if (named && !sameProject(named, pinned)) return {
18412
+ refused: envelope_envelope({
18413
+ ok: false,
18414
+ command: name,
18415
+ status: "project-pinned",
18416
+ error: {
18417
+ code: "E_TOOL_DISABLED",
18418
+ message: `This server is pinned to ${pinned} and was asked to act on ${named}.`
18419
+ },
18420
+ value: {
18421
+ pinned,
18422
+ named
18423
+ },
18424
+ hint: `Work on ${named} through an MCP server configured with --project ${named}, or change this server's --project (or ${PROJECT_PIN_ENV}).`
18425
+ })
18426
+ };
18427
+ const properties = inputSchema.properties ?? {};
18428
+ const statusOnly = "extension_auth" === name && "status" === args.action;
18429
+ if (named || statusOnly || !("project" in properties)) return {
18430
+ args
18431
+ };
18432
+ return {
18433
+ args: {
18434
+ ...args,
18435
+ project: pinned
18436
+ }
18437
+ };
18438
+ }
18141
18439
  const tools = [
18142
18440
  create_namespaceObject,
18143
18441
  templates_namespaceObject,
@@ -18235,8 +18533,8 @@ function createServer(options = DEFAULT_SERVER_OPTIONS) {
18235
18533
  ],
18236
18534
  isError: true
18237
18535
  };
18238
- const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
18239
- const disabled = disabledToolEnvelope(name, normalizedArgs, options);
18536
+ const aliasedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
18537
+ const disabled = disabledToolEnvelope(name, aliasedArgs, options);
18240
18538
  if (disabled) return {
18241
18539
  content: [
18242
18540
  {
@@ -18246,6 +18544,17 @@ function createServer(options = DEFAULT_SERVER_OPTIONS) {
18246
18544
  ],
18247
18545
  isError: true
18248
18546
  };
18547
+ const pinned = pinProjectArgs(name, aliasedArgs, tool.schema.inputSchema, options);
18548
+ if ("refused" in pinned) return {
18549
+ content: [
18550
+ {
18551
+ type: "text",
18552
+ text: pinned.refused
18553
+ }
18554
+ ],
18555
+ isError: true
18556
+ };
18557
+ const normalizedArgs = pinned.args;
18249
18558
  const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
18250
18559
  if (issues.length) return {
18251
18560
  content: [
@@ -18284,6 +18593,7 @@ function createServer(options = DEFAULT_SERVER_OPTIONS) {
18284
18593
  }
18285
18594
  async function startServer(options = DEFAULT_SERVER_OPTIONS) {
18286
18595
  installCarrierExitCleanup();
18596
+ if (options.project) process.env.EXTENSION_DEV_PROJECT = options.project;
18287
18597
  const server = createServer(options);
18288
18598
  const transport = new StdioServerTransport();
18289
18599
  await server.connect(transport);
@@ -0,0 +1,31 @@
1
+ export type ClientId = "claude-code" | "cursor" | "vscode" | "codex" | "json";
2
+ export type Reach = "everything" | "no-ship" | "local";
3
+ export interface ClientInfo {
4
+ id: ClientId;
5
+ label: string;
6
+ subtitle?: string;
7
+ }
8
+ export declare const CLIENTS: readonly ClientInfo[];
9
+ export interface Recipe {
10
+ client: ClientId;
11
+ command?: string;
12
+ config?: {
13
+ language: "json" | "toml";
14
+ path: string;
15
+ text: string;
16
+ };
17
+ deeplink?: string;
18
+ login: string;
19
+ }
20
+ export interface RecipeInput {
21
+ client: ClientId;
22
+ reach: Reach;
23
+ strictApproval: boolean;
24
+ project: string;
25
+ }
26
+ export declare const SERVER_NAME = "extension-dev";
27
+ export declare const PACKAGE = "@extension.dev/mcp";
28
+ export declare function serverArgs(input: Omit<RecipeInput, "client">): string[];
29
+ export declare function serverEnv(input: Omit<RecipeInput, "client">): Record<string, string>;
30
+ export declare function loginCommand(project: string): string;
31
+ export declare function buildRecipe(input: RecipeInput): Recipe;
@@ -0,0 +1,4 @@
1
+ export declare const README_SETUP_START = "<!-- setup:start (generated from src/clients, run pnpm readme:clients) -->";
2
+ export declare const README_SETUP_END = "<!-- setup:end -->";
3
+ export declare function renderReadmeSetup(): string;
4
+ export declare function spliceReadmeSetup(readme: string): string;
@@ -18,6 +18,10 @@ export interface CredentialSelector {
18
18
  }
19
19
  export declare function credentialKey(workspaceSlug: string, projectSlug: string): string;
20
20
  export declare function readCredentialStore(): CredentialStore | null;
21
+ export declare const PROJECT_PIN_ENV = "EXTENSION_DEV_PROJECT";
22
+ export declare function pinnedProject(): string;
23
+ export declare function isProjectRef(value: string): boolean;
24
+ export declare function sameProject(named: string, pinned: string): boolean;
21
25
  export declare function readCredentials(selector?: CredentialSelector): StoredCredentials | null;
22
26
  export declare function listCredentials(): Array<StoredCredentials & {
23
27
  key: string;
@@ -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[];
@@ -18,6 +18,7 @@ export declare const TOOL_POLICY: Record<string, ToolPolicy>;
18
18
  export interface ServerOptions {
19
19
  features: FeatureGroup[];
20
20
  noShip: boolean;
21
+ project?: string;
21
22
  }
22
23
  export declare const DEFAULT_SERVER_OPTIONS: ServerOptions;
23
24
  export declare function isServerFlag(arg: string): boolean;
@@ -30,4 +31,9 @@ export declare function resolveServerOptions(argv: string[], env: Record<string,
30
31
  };
31
32
  export declare function isToolListed(name: string, options: ServerOptions): boolean;
32
33
  export declare function disabledToolEnvelope(name: string, args: Args, options: ServerOptions): string | null;
34
+ export declare function pinProjectArgs(name: string, args: Args, inputSchema: Record<string, unknown>, options: ServerOptions): {
35
+ args: Args;
36
+ } | {
37
+ refused: string;
38
+ };
33
39
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "10.10.3",
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",
@@ -26,6 +26,10 @@
26
26
  ".": {
27
27
  "types": "./dist/module.d.ts",
28
28
  "import": "./dist/module.js"
29
+ },
30
+ "./clients": {
31
+ "types": "./dist/src/clients/index.d.ts",
32
+ "import": "./dist/clients.js"
29
33
  }
30
34
  },
31
35
  "main": "./dist/module.js",
@@ -46,6 +50,7 @@
46
50
  ],
47
51
  "scripts": {
48
52
  "clean": "rm -rf dist",
53
+ "readme:clients": "UPDATE_README=1 vitest run src/__tests__/readme-clients.test.ts",
49
54
  "watch": "rslib build --watch",
50
55
  "compile": "rslib build",
51
56
  "build": "pnpm run compile",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "10.10.3",
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.3",
16
+ "version": "10.10.5",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"