@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +38 -0
- package/README.md +70 -3
- package/bin/extension-mcp.js +1 -0
- package/claude/commands/extension-publish.md +30 -53
- package/dist/clients.js +157 -0
- package/dist/module.js +321 -11
- package/dist/src/clients/index.d.ts +31 -0
- package/dist/src/clients/readme.d.ts +4 -0
- package/dist/src/lib/credentials.d.ts +4 -0
- package/dist/src/lib/store-review.d.ts +23 -0
- package/dist/src/lib/tool-policy.d.ts +6 -0
- package/package.json +6 -1
- package/server.json +2 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "extension-mcp",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "MCP tools for browser extension development: scaffold from 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.
|
|
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
|
+
"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
|
[](https://cursor.com/en/install-mcp?name=extension-dev&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAZXh0ZW5zaW9uLmRldi9tY3AiXX0%3D)
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
`.cursor/mcp.json`:
|
|
66
68
|
|
|
67
69
|
```json
|
|
68
70
|
{
|
|
69
71
|
"mcpServers": {
|
|
70
72
|
"extension-dev": {
|
|
71
73
|
"command": "npx",
|
|
72
|
-
"args": [
|
|
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:
|
package/bin/extension-mcp.js
CHANGED
|
@@ -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:
|
|
3
|
-
argument-hint: "[chrome|firefox|
|
|
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
|
-
|
|
6
|
+
Take the current extension to store review. The user said: $ARGUMENTS
|
|
7
7
|
|
|
8
8
|
## Parse arguments
|
|
9
9
|
|
|
10
|
-
Default to `
|
|
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. **
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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/clients.js
ADDED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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("--
|
|
18326
|
+
if (!arg.startsWith("--project=")) return {
|
|
18072
18327
|
ok: false,
|
|
18073
18328
|
message: `Unknown flag "${arg}".`
|
|
18074
18329
|
};
|
|
18075
|
-
|
|
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
|
|
18239
|
-
const disabled = disabledToolEnvelope(name,
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
+
"version": "10.10.5",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|