@extension.dev/mcp 10.10.2 → 10.10.4

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.2",
13
+ "version": "10.10.4",
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.2",
4
+ "version": "10.10.4",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 10.10.4
4
+
5
+ - `--project <workspace>/<project>` (env `EXTENSION_DEV_PROJECT`) pins a
6
+ server to one project. Every unnamed credential read takes that
7
+ project's login instead of the most recent one, an agent signed in to
8
+ several projects can no longer act on the wrong one, and a call that
9
+ names a different project is refused with `status: "project-pinned"`.
10
+ A pinned project with no stored login falls back to
11
+ `EXTENSION_DEV_TOKEN`, so CI keeps working.
12
+ - New `@extension.dev/mcp/clients` export: `CLIENTS` and
13
+ `buildRecipe({ client, reach, strictApproval, project })` build the
14
+ exact setup for Claude Code, Cursor, VS Code, Codex and any
15
+ `mcpServers` client. The README's Setup section is generated from it
16
+ (`pnpm readme:clients`, checked by a spec), and the extension.dev
17
+ console's Connect dialog renders the same recipes, so the instructions
18
+ cannot drift. It never emits a setting that turns approvals off.
19
+
20
+ ## 10.10.3
21
+
22
+ - The engine pin moves off the canary to Extension.js `4.1.31` stable:
23
+ `extension_dev`, `extension_start` and `extension_build` spawn that
24
+ release when a project has no engine of its own, and the nightly
25
+ engine-pin check reads it against `latest` again.
26
+
27
+ - A human now approves a real store submission and a promotion to stable
28
+ before either runs, by default. The first call answers
29
+ `approval-required` with an `approvalUrl` on extension.dev
30
+ (`/device/approve/<id>`) and an `approvalId`; once a workspace member
31
+ approves (an owner, for a submission), the same call with that
32
+ `approvalId` runs exactly that action, once. Promotions to other channels
33
+ and share revokes stay ungated unless `EXTENSION_DEV_APPROVAL_GATE=1`;
34
+ `EXTENSION_DEV_APPROVAL_GATE=0` turns the default off. Whatever the
35
+ setting, a platform that answers `APPROVAL_REQUIRED` now gets the same
36
+ approval flow instead of a bare failure.
37
+
38
+ - Two server flags narrow what an agent can reach. `--features=local`
39
+ or `--features=platform` (env `EXTENSION_DEV_FEATURES`) lists one tool
40
+ group; `local` alone is about 40% smaller. `--no-ship` (env
41
+ `EXTENSION_DEV_NO_SHIP`) hides `extension_publish` and
42
+ `extension_release_promote` and refuses a real submit, a share link and
43
+ a share revoke, while dry runs and listing keep working. A refused call
44
+ answers the new code `E_TOOL_DISABLED` with the flag to change. Both
45
+ default to everything on, so no existing setup changes.
46
+ - Every tool now carries MCP annotations (`readOnlyHint`,
47
+ `destructiveHint`, `idempotentHint`, `openWorldHint`) from one policy
48
+ table, so clients can auto-approve reads and ask before the rest. A
49
+ merged tool takes its worst action: `extension_shares` is destructive
50
+ because revoke is.
51
+ - A device-flow step waiting on a human (`extension_auth` login,
52
+ `extension_project_create`, `extension_workspace_create`) now answers
53
+ `ok: true` with `status: "authorization-pending"` and no error, instead
54
+ of `ok: false` with `E_AUTH_PENDING`, so clients stop rendering the
55
+ first login step as a failure. The approval link and code stay in
56
+ `hint` and `value`; `E_AUTH_PENDING` is retired.
57
+
3
58
  ## 10.10.2
4
59
 
5
60
  - The engine pin moves to the Extension.js canary
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,14 +64,103 @@ 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`:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "extension-dev": {
73
+ "command": "npx",
74
+ "args": [
75
+ "@extension.dev/mcp"
76
+ ]
77
+ }
78
+ }
79
+ }
80
+ ```
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
+
145
+ ### Choosing what the agent can reach
146
+
147
+ Two flags (or environment variables) narrow the server before an agent sees it:
148
+
149
+ - `--features=local` exposes only the tools that work on this machine (create, run, inspect, build), which also cuts the tool list by about 40%. `--features=platform` exposes only the extension.dev account, share, release and store tools. Both are on by default. Env: `EXTENSION_DEV_FEATURES`.
150
+ - `--no-ship` keeps everything that stays on this machine and refuses every call that reaches other people: `extension_publish`, `extension_release_promote`, `extension_submit` with `dryRun: false`, `extension_preview_web` with `share: true`, and a share revoke. Dry runs and share listing still work. Env: `EXTENSION_DEV_NO_SHIP=1`.
151
+
152
+ A refused call answers `E_TOOL_DISABLED` with the flag to change.
153
+
154
+ A real store submission and a promotion to stable also wait for a person by default: the first call answers `approval-required` with a link on extension.dev, a workspace member approves exactly that action, and the same call with the returned `approvalId` runs it once. `EXTENSION_DEV_APPROVAL_GATE=1` extends this to every promotion and share revoke; `EXTENSION_DEV_APPROVAL_GATE=0` turns it off.
155
+
156
+ Every tool also carries MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), so clients can auto-approve reads and ask before the rest.
66
157
 
67
158
  ```json
68
159
  {
69
160
  "mcpServers": {
70
161
  "extension-dev": {
71
162
  "command": "npx",
72
- "args": ["@extension.dev/mcp"]
163
+ "args": ["@extension.dev/mcp", "--no-ship"]
73
164
  }
74
165
  }
75
166
  }
@@ -8,7 +8,7 @@
8
8
  // Apache License 2.0 (c) 2026 Cezar Augusto and the extension.dev collaborators
9
9
 
10
10
  import {createRequire} from 'node:module'
11
- import {startServer, runCli} from '../dist/module.js'
11
+ import {startServer, runCli, isServerFlag, resolveServerOptions} from '../dist/module.js'
12
12
 
13
13
  const require = createRequire(import.meta.url)
14
14
  const {version} = require('../package.json')
@@ -16,18 +16,29 @@ const {version} = require('../package.json')
16
16
  const usage = `@extension.dev/mcp ${version}
17
17
 
18
18
  Usage:
19
- extension-mcp Start the MCP server on stdio
19
+ extension-mcp [flags] Start the MCP server on stdio
20
20
  extension-mcp login [flags] Device login at extension.dev
21
21
  extension-mcp logout Remove the stored credentials
22
22
  extension-mcp whoami Show the stored identity
23
23
  extension-mcp release [...] Headless release commands
24
24
  extension-mcp --version Print the version
25
+
26
+ Server flags:
27
+ --features=local,platform Tool groups to expose (default: both)
28
+ --no-ship Refuse publish, promote, real submits,
29
+ share links and share revokes
30
+ --project <workspace>/<proj> Use only this project's login
25
31
  `
26
32
 
27
33
  const [, , cmd, ...rest] = process.argv
28
34
 
29
- if (cmd === undefined) {
30
- startServer()
35
+ if (cmd === undefined || isServerFlag(cmd)) {
36
+ const resolved = resolveServerOptions(process.argv.slice(2), process.env)
37
+ if (!resolved.ok) {
38
+ process.stderr.write(`${resolved.message}\n\n${usage}`)
39
+ process.exit(1)
40
+ }
41
+ startServer(resolved.options)
31
42
  } else if (['login', 'logout', 'whoami', 'release'].includes(cmd)) {
32
43
  runCli(cmd, rest)
33
44
  .then((code) => process.exit(code))
@@ -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.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { startServer, runCli, version } from "./src/index";
2
+ export { isServerFlag, resolveServerOptions } from "./src/lib/tool-policy";
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.2","El":{"OP":"4.1.31-canary.1791055414.4bbb683a"}}');
239
+ var package_namespaceObject = JSON.parse('{"rE":"10.10.4","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);
@@ -1986,13 +1998,9 @@ function pendingEnvelope(start) {
1986
1998
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
1987
1999
  const message = hasCompleteLink ? `Open ${complete} and approve creating the project (code ${start.userCode} is pre-filled), then call extension_project_create again with this deviceCode and the same arguments. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri}, enter code ${start.userCode}, approve creating the project, then call extension_project_create again with this deviceCode and the same arguments.`;
1988
2000
  return envelope_envelope({
1989
- ok: false,
2001
+ ok: true,
1990
2002
  command: project_create_COMMAND,
1991
2003
  status: "authorization-pending",
1992
- error: {
1993
- code: "E_AUTH_PENDING",
1994
- message
1995
- },
1996
2004
  value: {
1997
2005
  userCode: start.userCode,
1998
2006
  verificationUri: start.verificationUri,
@@ -2131,17 +2139,14 @@ async function project_create_handler(args) {
2131
2139
  async function finishFromPoll(poll, ctx) {
2132
2140
  if (!poll.ok) {
2133
2141
  if ("pending" === poll.reason) return envelope_envelope({
2134
- ok: false,
2142
+ ok: true,
2135
2143
  command: project_create_COMMAND,
2136
2144
  status: "authorization-pending",
2137
- error: {
2138
- code: "E_AUTH_PENDING",
2139
- message: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call extension_project_create again with this same deviceCode.`
2140
- },
2141
2145
  value: {
2142
2146
  verificationUri: ctx.verificationUri,
2143
2147
  deviceCode: ctx.deviceCode
2144
- }
2148
+ },
2149
+ hint: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call extension_project_create again with this same deviceCode.`
2145
2150
  });
2146
2151
  if ("denied" === poll.reason) return fail("CreateDenied", "Creating the project was denied at extension.dev/device.", "create-denied", "E_AUTH_DENIED");
2147
2152
  if ("expired" === poll.reason) return fail("CreateExpired", "The device code expired. Run extension_project_create again to restart.", "create-expired", "E_AUTH_EXPIRED");
@@ -2332,13 +2337,9 @@ function workspace_create_pendingEnvelope(start) {
2332
2337
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
2333
2338
  const message = hasCompleteLink ? `Open ${complete} and approve creating the workspace (code ${start.userCode} is pre-filled), then call ${workspace_create_COMMAND} again with this deviceCode and the same arguments. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}. The GitHub account that approves becomes the workspace owner.` : `Open ${start.verificationUri}, enter code ${start.userCode}, approve creating the workspace, then call ${workspace_create_COMMAND} again with this deviceCode and the same arguments. The GitHub account that approves becomes the workspace owner.`;
2334
2339
  return envelope_envelope({
2335
- ok: false,
2340
+ ok: true,
2336
2341
  command: workspace_create_COMMAND,
2337
2342
  status: "authorization-pending",
2338
- error: {
2339
- code: "E_AUTH_PENDING",
2340
- message
2341
- },
2342
2343
  value: {
2343
2344
  userCode: start.userCode,
2344
2345
  verificationUri: start.verificationUri,
@@ -2419,17 +2420,14 @@ async function workspace_create_handler(args) {
2419
2420
  async function workspace_create_finishFromPoll(poll, ctx) {
2420
2421
  if (!poll.ok) {
2421
2422
  if ("pending" === poll.reason) return envelope_envelope({
2422
- ok: false,
2423
+ ok: true,
2423
2424
  command: workspace_create_COMMAND,
2424
2425
  status: "authorization-pending",
2425
- error: {
2426
- code: "E_AUTH_PENDING",
2427
- message: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call ${workspace_create_COMMAND} again with this same deviceCode.`
2428
- },
2429
2426
  value: {
2430
2427
  verificationUri: ctx.verificationUri,
2431
2428
  deviceCode: ctx.deviceCode
2432
- }
2429
+ },
2430
+ hint: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call ${workspace_create_COMMAND} again with this same deviceCode.`
2433
2431
  });
2434
2432
  if ("denied" === poll.reason) return workspace_create_fail("CreateDenied", "Creating the workspace was denied at extension.dev/device.", "create-denied", "E_AUTH_DENIED");
2435
2433
  if ("expired" === poll.reason) return workspace_create_fail("CreateExpired", `The device code expired. Run ${workspace_create_COMMAND} again to restart.`, "create-expired", "E_AUTH_EXPIRED");
@@ -7935,12 +7933,15 @@ async function openSidebarThroughGesture(projectPath, browser, refusal) {
7935
7933
  return actFrameJson(refusal);
7936
7934
  }
7937
7935
  function resolveToken(options = {}) {
7938
- const project = String(options.project ?? "").trim();
7936
+ const named = String(options.project ?? "").trim();
7937
+ const project = named || pinnedProject();
7939
7938
  if (project) {
7940
7939
  const creds = readValidCredentials(void 0, {
7941
7940
  project
7942
7941
  });
7943
- return creds?.token ? String(creds.token).trim() : "";
7942
+ if (creds?.token) return String(creds.token).trim();
7943
+ if (named) return "";
7944
+ return String(process.env.EXTENSION_DEV_TOKEN || "").trim();
7944
7945
  }
7945
7946
  const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
7946
7947
  if (fromEnv) return fromEnv;
@@ -8204,6 +8205,9 @@ async function revokeArtifact(options) {
8204
8205
  error: {
8205
8206
  name: "SharesRevokeError",
8206
8207
  status: res.status,
8208
+ ..."string" == typeof data?.code ? {
8209
+ code: data.code
8210
+ } : {},
8207
8211
  message: `Revoking ${options.artifactId} failed (${res.status}): ${data?.message || "unknown error"}`
8208
8212
  }
8209
8213
  };
@@ -9176,9 +9180,29 @@ const APPROVAL_GATE_ENV = "EXTENSION_DEV_APPROVAL_GATE";
9176
9180
  const APPROVAL_REQUIRED_STATUS = "approval-required";
9177
9181
  const APPROVAL_PENDING_STATUS = "approval-pending";
9178
9182
  const APPROVAL_REJECTED_STATUS = "approval-rejected";
9179
- function approvalGateEnabled() {
9183
+ function approvalGateEnabled(defaultOn = false) {
9180
9184
  const raw = String(process.env[APPROVAL_GATE_ENV] || "").trim().toLowerCase();
9181
- return "" !== raw && "0" !== raw && "false" !== raw && "off" !== raw;
9185
+ if ("" === raw) return defaultOn;
9186
+ return "0" !== raw && "false" !== raw && "off" !== raw;
9187
+ }
9188
+ function platformRequiresApproval(res, data) {
9189
+ return 403 === res.status && !!data && "object" == typeof data && "APPROVAL_REQUIRED" === data.code;
9190
+ }
9191
+ async function requestApprovalAfterRefusal(input) {
9192
+ const gate = await evaluateApproval({
9193
+ ...input,
9194
+ approvalId: void 0,
9195
+ enabled: true
9196
+ });
9197
+ return gate.blocked ? gate.envelope : envelope_envelope({
9198
+ ok: false,
9199
+ command: input.command,
9200
+ status: APPROVAL_REQUIRED_STATUS,
9201
+ error: {
9202
+ code: "E_APPROVAL_REQUIRED",
9203
+ message: "The platform requires an approval for this action."
9204
+ }
9205
+ });
9182
9206
  }
9183
9207
  function canonicalScope(scope) {
9184
9208
  return Object.keys(scope).sort().map((key)=>{
@@ -9666,18 +9690,19 @@ async function revokeShare(args) {
9666
9690
  const token = resolveToken({
9667
9691
  project: args.project
9668
9692
  });
9693
+ const gateInput = {
9694
+ command: "extension_shares",
9695
+ action: "extension_shares.revoke",
9696
+ scope: {
9697
+ artifactId: ref
9698
+ },
9699
+ description: `Permanently revoke share ${ref}, killing the link for everyone with no way to restore it.`,
9700
+ approvalId: args.approvalId,
9701
+ token: token || "",
9702
+ api: args.api
9703
+ };
9669
9704
  if (token) {
9670
- const gate = await evaluateApproval({
9671
- command: "extension_shares",
9672
- action: "extension_shares.revoke",
9673
- scope: {
9674
- artifactId: ref
9675
- },
9676
- description: `Permanently revoke share ${ref}, killing the link for everyone with no way to restore it.`,
9677
- approvalId: args.approvalId,
9678
- token,
9679
- api: args.api
9680
- });
9705
+ const gate = await evaluateApproval(gateInput);
9681
9706
  if (gate.blocked) return gate.envelope;
9682
9707
  }
9683
9708
  const result = await revokeArtifact({
@@ -9703,6 +9728,7 @@ async function revokeShare(args) {
9703
9728
  artifactId: ref
9704
9729
  }
9705
9730
  });
9731
+ if (token && !args.approvalId && 403 === result.error.status && "APPROVAL_REQUIRED" === result.error.code) return requestApprovalAfterRefusal(gateInput);
9706
9732
  const isAuth = "SharesAuthError" === result.error.name;
9707
9733
  return envelope_envelope({
9708
9734
  ok: false,
@@ -15393,7 +15419,7 @@ async function release_promote_handler(args) {
15393
15419
  const url = `${apiCheck.base}/api/cli/release/promote`;
15394
15420
  const sourceChannel = args.sourceChannel ? String(args.sourceChannel).trim() : "";
15395
15421
  const gateBrowsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim()).filter(Boolean);
15396
- const gate = await evaluateApproval({
15422
+ const gateInput = {
15397
15423
  command: "extension_release_promote",
15398
15424
  action: "extension_release_promote",
15399
15425
  scope: {
@@ -15412,6 +15438,10 @@ async function release_promote_handler(args) {
15412
15438
  approvalId: args.approvalId,
15413
15439
  token,
15414
15440
  api: args.api
15441
+ };
15442
+ const gate = await evaluateApproval({
15443
+ ...gateInput,
15444
+ enabled: approvalGateEnabled("stable" === channel)
15415
15445
  });
15416
15446
  if (gate.blocked) return gate.envelope;
15417
15447
  const body = {
@@ -15456,6 +15486,7 @@ async function release_promote_handler(args) {
15456
15486
  buildId
15457
15487
  }
15458
15488
  });
15489
+ if (!args.approvalId && platformRequiresApproval(res, data)) return requestApprovalAfterRefusal(gateInput);
15459
15490
  const code = "string" == typeof data?.code ? data.code : void 0;
15460
15491
  const enrich = {};
15461
15492
  let hint = "";
@@ -16090,21 +16121,25 @@ async function submit_handler(args) {
16090
16121
  };
16091
16122
  if (args.channel) body.channel = String(args.channel).trim();
16092
16123
  if (args.version) body.version = String(args.version).trim();
16124
+ const gateInput = {
16125
+ command: "extension_submit",
16126
+ action: "extension_submit",
16127
+ scope: {
16128
+ browsers: [
16129
+ ...browsers
16130
+ ].sort(),
16131
+ buildSha: buildSha.toLowerCase(),
16132
+ channel
16133
+ },
16134
+ description: `Submit build ${buildSha} to ${browsers.join(", ")} for store review.`,
16135
+ approvalId: args.approvalId,
16136
+ token,
16137
+ api: args.api
16138
+ };
16093
16139
  if (!dryRun) {
16094
16140
  const gate = await evaluateApproval({
16095
- command: "extension_submit",
16096
- action: "extension_submit",
16097
- scope: {
16098
- browsers: [
16099
- ...browsers
16100
- ].sort(),
16101
- buildSha: buildSha.toLowerCase(),
16102
- channel
16103
- },
16104
- description: `Submit build ${buildSha} to ${browsers.join(", ")} for store review.`,
16105
- approvalId: args.approvalId,
16106
- token,
16107
- api: args.api
16141
+ ...gateInput,
16142
+ enabled: approvalGateEnabled(true)
16108
16143
  });
16109
16144
  if (gate.blocked) return gate.envelope;
16110
16145
  if (gate.approvalId) body.approvalId = gate.approvalId;
@@ -16144,6 +16179,7 @@ async function submit_handler(args) {
16144
16179
  dryRun
16145
16180
  }
16146
16181
  });
16182
+ if (!dryRun && !args.approvalId && platformRequiresApproval(res, data)) return requestApprovalAfterRefusal(gateInput);
16147
16183
  return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`, "submit-failed", "E_PLATFORM");
16148
16184
  }
16149
16185
  const warnings = Array.isArray(data?.warnings) ? [
@@ -16793,13 +16829,9 @@ function login_pending(start) {
16793
16829
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
16794
16830
  const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_auth (action: login) again with this deviceCode and the same project. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_auth (action: login) again with this deviceCode and the same project.`;
16795
16831
  return envelope_envelope({
16796
- ok: false,
16832
+ ok: true,
16797
16833
  command: "extension_auth",
16798
16834
  status: "authorization-pending",
16799
- error: {
16800
- code: "E_AUTH_PENDING",
16801
- message
16802
- },
16803
16835
  value: {
16804
16836
  userCode: start.userCode,
16805
16837
  verificationUri: start.verificationUri,
@@ -16818,13 +16850,9 @@ function login_pending(start) {
16818
16850
  function resumePending(deviceCode, verificationUri) {
16819
16851
  const message = `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_auth (action: login) again with this same deviceCode and the same project.`;
16820
16852
  return envelope_envelope({
16821
- ok: false,
16853
+ ok: true,
16822
16854
  command: "extension_auth",
16823
16855
  status: "authorization-pending",
16824
- error: {
16825
- code: "E_AUTH_PENDING",
16826
- message
16827
- },
16828
16856
  value: {
16829
16857
  verificationUri,
16830
16858
  deviceCode,
@@ -17843,6 +17871,334 @@ function inputValidationError(toolName, issues, inputSchema) {
17843
17871
  }
17844
17872
  });
17845
17873
  }
17874
+ const FEATURE_GROUPS = [
17875
+ "local",
17876
+ "platform"
17877
+ ];
17878
+ const FEATURES_ENV = "EXTENSION_DEV_FEATURES";
17879
+ const NO_SHIP_ENV = "EXTENSION_DEV_NO_SHIP";
17880
+ const reads = (openWorld = false)=>({
17881
+ readOnlyHint: true,
17882
+ destructiveHint: false,
17883
+ idempotentHint: true,
17884
+ openWorldHint: openWorld
17885
+ });
17886
+ const acts = (over = {})=>({
17887
+ readOnlyHint: false,
17888
+ destructiveHint: false,
17889
+ idempotentHint: false,
17890
+ openWorldHint: false,
17891
+ ...over
17892
+ });
17893
+ const TOOL_POLICY = {
17894
+ extension_create: {
17895
+ group: "local",
17896
+ annotations: acts({
17897
+ openWorldHint: true
17898
+ })
17899
+ },
17900
+ extension_templates: {
17901
+ group: "local",
17902
+ annotations: reads(true)
17903
+ },
17904
+ extension_add_feature: {
17905
+ group: "local",
17906
+ annotations: reads()
17907
+ },
17908
+ extension_build: {
17909
+ group: "local",
17910
+ annotations: acts({
17911
+ idempotentHint: true
17912
+ })
17913
+ },
17914
+ extension_dev: {
17915
+ group: "local",
17916
+ annotations: acts()
17917
+ },
17918
+ extension_start: {
17919
+ group: "local",
17920
+ annotations: acts()
17921
+ },
17922
+ extension_wait: {
17923
+ group: "local",
17924
+ annotations: reads()
17925
+ },
17926
+ extension_stop: {
17927
+ group: "local",
17928
+ annotations: acts({
17929
+ idempotentHint: true
17930
+ })
17931
+ },
17932
+ extension_manifest_validate: {
17933
+ group: "local",
17934
+ annotations: reads()
17935
+ },
17936
+ extension_theme_verify: {
17937
+ group: "local",
17938
+ annotations: reads()
17939
+ },
17940
+ extension_analyze: {
17941
+ group: "local",
17942
+ annotations: reads()
17943
+ },
17944
+ extension_assert: {
17945
+ group: "local",
17946
+ annotations: acts({
17947
+ idempotentHint: true
17948
+ })
17949
+ },
17950
+ extension_inspect: {
17951
+ group: "local",
17952
+ annotations: acts({
17953
+ idempotentHint: true
17954
+ })
17955
+ },
17956
+ extension_dom_snapshot: {
17957
+ group: "local",
17958
+ annotations: reads()
17959
+ },
17960
+ extension_list_extensions: {
17961
+ group: "local",
17962
+ annotations: reads()
17963
+ },
17964
+ extension_logs: {
17965
+ group: "local",
17966
+ annotations: reads()
17967
+ },
17968
+ extension_doctor: {
17969
+ group: "local",
17970
+ annotations: reads()
17971
+ },
17972
+ extension_eval: {
17973
+ group: "local",
17974
+ annotations: acts({
17975
+ destructiveHint: true,
17976
+ openWorldHint: true
17977
+ })
17978
+ },
17979
+ extension_storage: {
17980
+ group: "local",
17981
+ annotations: acts({
17982
+ destructiveHint: true
17983
+ })
17984
+ },
17985
+ extension_reload: {
17986
+ group: "local",
17987
+ annotations: acts({
17988
+ idempotentHint: true
17989
+ })
17990
+ },
17991
+ extension_open: {
17992
+ group: "local",
17993
+ annotations: acts({
17994
+ openWorldHint: true
17995
+ })
17996
+ },
17997
+ extension_browsers: {
17998
+ group: "local",
17999
+ annotations: acts({
18000
+ destructiveHint: true,
18001
+ openWorldHint: true
18002
+ })
18003
+ },
18004
+ extension_auth: {
18005
+ group: "platform",
18006
+ annotations: acts({
18007
+ openWorldHint: true
18008
+ })
18009
+ },
18010
+ extension_workspace_create: {
18011
+ group: "platform",
18012
+ annotations: acts({
18013
+ openWorldHint: true
18014
+ })
18015
+ },
18016
+ extension_project_create: {
18017
+ group: "platform",
18018
+ annotations: acts({
18019
+ openWorldHint: true
18020
+ })
18021
+ },
18022
+ extension_preview_web: {
18023
+ group: "platform",
18024
+ annotations: acts({
18025
+ openWorldHint: true
18026
+ }),
18027
+ ships: (args)=>true === args.share
18028
+ },
18029
+ extension_shares: {
18030
+ group: "platform",
18031
+ annotations: acts({
18032
+ destructiveHint: true,
18033
+ openWorldHint: true
18034
+ }),
18035
+ ships: (args)=>"revoke" === args.action
18036
+ },
18037
+ extension_publish: {
18038
+ group: "platform",
18039
+ annotations: acts({
18040
+ openWorldHint: true
18041
+ }),
18042
+ ships: "always"
18043
+ },
18044
+ extension_release_status: {
18045
+ group: "platform",
18046
+ annotations: reads(true)
18047
+ },
18048
+ extension_release_promote: {
18049
+ group: "platform",
18050
+ annotations: acts({
18051
+ destructiveHint: true,
18052
+ openWorldHint: true
18053
+ }),
18054
+ ships: "always"
18055
+ },
18056
+ extension_submit: {
18057
+ group: "platform",
18058
+ annotations: acts({
18059
+ destructiveHint: true,
18060
+ openWorldHint: true
18061
+ }),
18062
+ ships: (args)=>false === args.dryRun
18063
+ }
18064
+ };
18065
+ const DEFAULT_SERVER_OPTIONS = {
18066
+ features: [
18067
+ ...FEATURE_GROUPS
18068
+ ],
18069
+ noShip: false
18070
+ };
18071
+ const truthy = (raw)=>{
18072
+ const value = String(raw ?? "").trim().toLowerCase();
18073
+ return "" !== value && "0" !== value && "false" !== value && "off" !== value;
18074
+ };
18075
+ function isServerFlag(arg) {
18076
+ return "--no-ship" === arg || "--features" === arg || arg.startsWith("--features=") || "--project" === arg || arg.startsWith("--project=");
18077
+ }
18078
+ function resolveServerOptions(argv, env) {
18079
+ let rawFeatures = env[FEATURES_ENV];
18080
+ let noShip = truthy(env[NO_SHIP_ENV]);
18081
+ let project = String(env[PROJECT_PIN_ENV] || "").trim().toLowerCase();
18082
+ for(let i = 0; i < argv.length; i++){
18083
+ const arg = argv[i];
18084
+ if ("--no-ship" === arg) noShip = true;
18085
+ else if ("--features" === arg) rawFeatures = argv[++i] ?? "";
18086
+ else if (arg.startsWith("--features=")) rawFeatures = arg.slice(11);
18087
+ else if ("--project" === arg) project = String(argv[++i] ?? "").trim().toLowerCase();
18088
+ else {
18089
+ if (!arg.startsWith("--project=")) return {
18090
+ ok: false,
18091
+ message: `Unknown flag "${arg}".`
18092
+ };
18093
+ project = arg.slice(10).trim().toLowerCase();
18094
+ }
18095
+ }
18096
+ if (project && !isProjectRef(project)) return {
18097
+ ok: false,
18098
+ message: `--project takes "<workspace>/<project>", got "${project}".`
18099
+ };
18100
+ const pin = project ? {
18101
+ project
18102
+ } : {};
18103
+ if (void 0 === rawFeatures || "" === rawFeatures.trim()) return {
18104
+ ok: true,
18105
+ options: {
18106
+ features: [
18107
+ ...FEATURE_GROUPS
18108
+ ],
18109
+ noShip,
18110
+ ...pin
18111
+ }
18112
+ };
18113
+ const names = rawFeatures.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);
18114
+ const unknown = names.filter((name)=>!FEATURE_GROUPS.includes(name));
18115
+ if (unknown.length) return {
18116
+ ok: false,
18117
+ message: `Unknown feature group ${unknown.map((n)=>`"${n}"`).join(", ")}. Use ${FEATURE_GROUPS.join(" or ")}, comma-separated.`
18118
+ };
18119
+ const features = FEATURE_GROUPS.filter((group)=>names.includes(group));
18120
+ return {
18121
+ ok: true,
18122
+ options: {
18123
+ features,
18124
+ noShip,
18125
+ ...pin
18126
+ }
18127
+ };
18128
+ }
18129
+ function isToolListed(name, options) {
18130
+ const policy = TOOL_POLICY[name];
18131
+ if (!policy) return false;
18132
+ if (!options.features.includes(policy.group)) return false;
18133
+ return !(options.noShip && "always" === policy.ships);
18134
+ }
18135
+ function disabledToolEnvelope(name, args, options) {
18136
+ const policy = TOOL_POLICY[name];
18137
+ if (!policy) return null;
18138
+ if (!options.features.includes(policy.group)) return envelope_envelope({
18139
+ ok: false,
18140
+ command: name,
18141
+ status: "tool-disabled",
18142
+ error: {
18143
+ code: "E_TOOL_DISABLED",
18144
+ message: `${name} is in the "${policy.group}" feature group, which this server was started without.`
18145
+ },
18146
+ value: {
18147
+ group: policy.group,
18148
+ features: options.features
18149
+ },
18150
+ hint: `Add ${policy.group} to --features (or ${FEATURES_ENV}) in this server's MCP config, or ask the user to.`
18151
+ });
18152
+ const ships = "always" === policy.ships || "function" == typeof policy.ships && policy.ships(args);
18153
+ if (options.noShip && ships) return envelope_envelope({
18154
+ ok: false,
18155
+ command: name,
18156
+ status: "tool-disabled",
18157
+ error: {
18158
+ code: "E_TOOL_DISABLED",
18159
+ message: `${name} with these arguments reaches people outside this machine, and this server was started in no-ship mode.`
18160
+ },
18161
+ value: {
18162
+ noShip: true
18163
+ },
18164
+ hint: `Dry runs, previews without share, and listing still work. To ship, the user removes --no-ship (or ${NO_SHIP_ENV}) from this server's MCP config.`
18165
+ });
18166
+ return null;
18167
+ }
18168
+ function pinProjectArgs(name, args, inputSchema, options) {
18169
+ const pinned = options.project;
18170
+ if (!pinned) return {
18171
+ args
18172
+ };
18173
+ const named = "string" == typeof args.project ? args.project : "";
18174
+ if (named && !sameProject(named, pinned)) return {
18175
+ refused: envelope_envelope({
18176
+ ok: false,
18177
+ command: name,
18178
+ status: "project-pinned",
18179
+ error: {
18180
+ code: "E_TOOL_DISABLED",
18181
+ message: `This server is pinned to ${pinned} and was asked to act on ${named}.`
18182
+ },
18183
+ value: {
18184
+ pinned,
18185
+ named
18186
+ },
18187
+ hint: `Work on ${named} through an MCP server configured with --project ${named}, or change this server's --project (or ${PROJECT_PIN_ENV}).`
18188
+ })
18189
+ };
18190
+ const properties = inputSchema.properties ?? {};
18191
+ const statusOnly = "extension_auth" === name && "status" === args.action;
18192
+ if (named || statusOnly || !("project" in properties)) return {
18193
+ args
18194
+ };
18195
+ return {
18196
+ args: {
18197
+ ...args,
18198
+ project: pinned
18199
+ }
18200
+ };
18201
+ }
17846
18202
  const tools = [
17847
18203
  create_namespaceObject,
17848
18204
  templates_namespaceObject,
@@ -17898,8 +18254,8 @@ function toolResultFrame(result) {
17898
18254
  }
17899
18255
  const toolMap = new Map();
17900
18256
  for (const tool of tools)toolMap.set(tool.schema.name, tool);
17901
- const SERVER_INSTRUCTIONS = "extension-dev runs, inspects, drives, builds and publishes browser extensions (Chrome, Edge, Firefox, Safari and the other Chromium and Gecko browsers) through Extension.js and extension.dev.\nWhen the ask is to run, start, wait for, watch, inspect, drive, test, debug or build a browser extension, search this server first and use its tools: extension_dev starts the dev session (allowEval: true also turns on control), extension_wait blocks until it is ready, extension_logs streams its console, extension_open opens a surface or a url, extension_dom_snapshot and extension_inspect read a live page, extension_eval runs code in a context, extension_build makes a store-ready bundle, extension_stop ends the session.\nThese replace hand-rolled ps, curl, remote-debugging-port lookups and CDP or Playwright scripts: the server already holds the session's debug port, the extension id and the session token.\nEvery tool answers one JSON envelope {ok, status, value, error, hint, warnings}; read hint and warnings before choosing the next call, and treat ok: false as the answer, not a transport error.";
17902
- function createServer() {
18257
+ const SERVER_INSTRUCTIONS = "extension-dev runs, inspects, drives, builds and publishes browser extensions (Chrome, Edge, Firefox, Safari and the other Chromium and Gecko browsers) through Extension.js and extension.dev.\nWhen the ask is to run, start, wait for, watch, inspect, drive, test, debug or build a browser extension, search this server first and use its tools: extension_dev starts the dev session (allowEval: true also turns on control), extension_wait blocks until it is ready, extension_logs streams its console, extension_open opens a surface or a url, extension_dom_snapshot and extension_inspect read a live page, extension_eval runs code in a context, extension_build makes a store-ready bundle, extension_stop ends the session.\nThese replace hand-rolled ps, curl, remote-debugging-port lookups and CDP or Playwright scripts: the server already holds the session's debug port, the extension id and the session token.\nEvery tool answers one JSON envelope {ok, status, value, error, hint, warnings}; read hint and warnings before choosing the next call, and treat ok: false as the answer, not a transport error. status authorization-pending is ok: true and means a human approves at the link in hint before you call again; it is not done yet.";
18258
+ function createServer(options = DEFAULT_SERVER_OPTIONS) {
17903
18259
  const server = new Server({
17904
18260
  name: "extension-dev",
17905
18261
  version: package_namespaceObject.rE
@@ -17910,10 +18266,11 @@ function createServer() {
17910
18266
  instructions: SERVER_INSTRUCTIONS
17911
18267
  });
17912
18268
  server.setRequestHandler(ListToolsRequestSchema, async ()=>({
17913
- tools: tools.map((t)=>({
18269
+ tools: tools.filter((t)=>isToolListed(t.schema.name, options)).map((t)=>({
17914
18270
  name: t.schema.name,
17915
18271
  description: t.schema.description,
17916
- inputSchema: t.schema.inputSchema
18272
+ inputSchema: t.schema.inputSchema,
18273
+ annotations: TOOL_POLICY[t.schema.name].annotations
17917
18274
  }))
17918
18275
  }));
17919
18276
  server.setRequestHandler(CallToolRequestSchema, async (request)=>{
@@ -17932,14 +18289,35 @@ function createServer() {
17932
18289
  message: `Unknown tool: ${name}`
17933
18290
  },
17934
18291
  value: {
17935
- availableTools: tools.map((t)=>t.schema.name)
18292
+ availableTools: tools.map((t)=>t.schema.name).filter((toolName)=>isToolListed(toolName, options))
17936
18293
  }
17937
18294
  })
17938
18295
  }
17939
18296
  ],
17940
18297
  isError: true
17941
18298
  };
17942
- const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
18299
+ const aliasedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
18300
+ const disabled = disabledToolEnvelope(name, aliasedArgs, options);
18301
+ if (disabled) return {
18302
+ content: [
18303
+ {
18304
+ type: "text",
18305
+ text: disabled
18306
+ }
18307
+ ],
18308
+ isError: true
18309
+ };
18310
+ const pinned = pinProjectArgs(name, aliasedArgs, tool.schema.inputSchema, options);
18311
+ if ("refused" in pinned) return {
18312
+ content: [
18313
+ {
18314
+ type: "text",
18315
+ text: pinned.refused
18316
+ }
18317
+ ],
18318
+ isError: true
18319
+ };
18320
+ const normalizedArgs = pinned.args;
17943
18321
  const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
17944
18322
  if (issues.length) return {
17945
18323
  content: [
@@ -17976,9 +18354,10 @@ function createServer() {
17976
18354
  });
17977
18355
  return server;
17978
18356
  }
17979
- async function startServer() {
18357
+ async function startServer(options = DEFAULT_SERVER_OPTIONS) {
17980
18358
  installCarrierExitCleanup();
17981
- const server = createServer();
18359
+ if (options.project) process.env.EXTENSION_DEV_PROJECT = options.project;
18360
+ const server = createServer(options);
17982
18361
  const transport = new StdioServerTransport();
17983
18362
  await server.connect(transport);
17984
18363
  }
@@ -18074,4 +18453,4 @@ async function runCli(cmd, args) {
18074
18453
  return 1;
18075
18454
  }
18076
18455
  var version_0 = package_namespaceObject.rE;
18077
- export { runCli, startServer, version_0 as version };
18456
+ export { isServerFlag, resolveServerOptions, runCli, startServer, version_0 as version };
@@ -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;
@@ -1,6 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { version } from "../package.json";
3
3
  export { version };
4
+ import { type ServerOptions } from "./lib/tool-policy";
4
5
  export interface ToolModule {
5
6
  schema: {
6
7
  name: string;
@@ -18,6 +19,6 @@ export declare function toolResultFrame(result: string): {
18
19
  isError?: boolean;
19
20
  };
20
21
  export declare const SERVER_INSTRUCTIONS: string;
21
- export declare function createServer(): Server;
22
- export declare function startServer(): Promise<void>;
22
+ export declare function createServer(options?: ServerOptions): Server;
23
+ export declare function startServer(options?: ServerOptions): Promise<void>;
23
24
  export declare function runCli(cmd: string, args: string[]): Promise<number>;
@@ -2,7 +2,11 @@ export declare const APPROVAL_GATE_ENV = "EXTENSION_DEV_APPROVAL_GATE";
2
2
  export declare const APPROVAL_REQUIRED_STATUS = "approval-required";
3
3
  export declare const APPROVAL_PENDING_STATUS = "approval-pending";
4
4
  export declare const APPROVAL_REJECTED_STATUS = "approval-rejected";
5
- export declare function approvalGateEnabled(): boolean;
5
+ export declare function approvalGateEnabled(defaultOn?: boolean): boolean;
6
+ export declare function platformRequiresApproval(res: {
7
+ status: number;
8
+ }, data: unknown): boolean;
9
+ export declare function requestApprovalAfterRefusal(input: ApprovalGateInput): Promise<string>;
6
10
  export type ApprovalScope = Record<string, string | string[]>;
7
11
  export declare function actionFingerprint(action: string, scope: ApprovalScope): string;
8
12
  export interface ApprovalGateInput {
@@ -55,6 +55,7 @@ export type ArtifactsOutcome<T> = {
55
55
  name: string;
56
56
  message: string;
57
57
  status?: number;
58
+ code?: string;
58
59
  };
59
60
  held?: boolean;
60
61
  body?: unknown;
@@ -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;
@@ -1,6 +1,6 @@
1
1
  export declare const ENVELOPE_SCHEMA: 1;
2
2
  export declare const DECISION_D6 = "The MCP tool name owns the envelope `command` key; forking the key to `tool` would fork the schema the checksum test pins.";
3
- export type ErrorCode = "E_AMBIGUOUS_TARGET" | "E_APPROVAL_PENDING" | "E_APPROVAL_REJECTED" | "E_APPROVAL_REQUIRED" | "E_AUTH_DENIED" | "E_AUTH_EXPIRED" | "E_AUTH_FAILED" | "E_AUTH_PENDING" | "E_AUTH_REQUIRED" | "E_BAD_HOST_URL" | "E_BAD_MANIFEST" | "E_BAD_REQUEST" | "E_BRIDGE" | "E_BROWSER_EXITED" | "E_BROWSER_INSTALL" | "E_BROWSER_UNINSTALL" | "E_BUILD_FAILED" | "E_CARRIER_IN_DIST" | "E_CDP" | "E_CLI" | "E_COMPILE" | "E_CONFIG" | "E_CONTRACT_ERROR" | "E_CONTROL_CHANNEL" | "E_CONTROL_ENVELOPE" | "E_ENGINE_TOO_OLD" | "E_ENTRYPOINT_MISSING" | "E_FIRST_COMPILE" | "E_INPUT_VALIDATION" | "E_INTERNAL" | "E_INVALID_BUNDLE_ID" | "E_LOGS_MISSING" | "E_MANIFEST_BLOCKING" | "E_MANIFEST_NOT_FOUND" | "E_NAVIGATE_ERROR" | "E_NAVIGATE_FAILED" | "E_NETWORK" | "E_NO_CONTROL_CHANNEL" | "E_NO_DIST" | "E_NO_EXTENSION_ID" | "E_NO_MATCHING_TARGET" | "E_NO_REFERENCE_TEMPLATE" | "E_NO_SESSION" | "E_NO_SURFACE_DOCUMENT" | "E_NO_TARGET" | "E_NOT_ATTACHED" | "E_PARSE" | "E_PLATFORM" | "E_PREVIEW_HOST_UNREACHABLE" | "E_PROFILE_LOCKED" | "E_RDP" | "E_SAFARI_ONLY_OPTION" | "E_EVAL" | "E_SCAFFOLD_FAILED" | "E_SCAFFOLD_INCOMPLETE" | "E_SESSION_EXISTS" | "E_SESSION_EXITED" | "E_STALE_CONTRACT" | "E_SURFACE_DID_NOT_OPEN" | "E_TEMPLATE_FETCH" | "E_TEMPLATE_NOT_FOUND" | "E_UNKNOWN_COMMAND" | "E_UNKNOWN_TOOL" | "E_UNSUPPORTED_BROWSER" | "E_WAIT_TIMEOUT";
3
+ export type ErrorCode = "E_AMBIGUOUS_TARGET" | "E_APPROVAL_PENDING" | "E_APPROVAL_REJECTED" | "E_APPROVAL_REQUIRED" | "E_AUTH_DENIED" | "E_AUTH_EXPIRED" | "E_AUTH_FAILED" | "E_AUTH_REQUIRED" | "E_BAD_HOST_URL" | "E_BAD_MANIFEST" | "E_BAD_REQUEST" | "E_BRIDGE" | "E_BROWSER_EXITED" | "E_BROWSER_INSTALL" | "E_BROWSER_UNINSTALL" | "E_BUILD_FAILED" | "E_CARRIER_IN_DIST" | "E_CDP" | "E_CLI" | "E_COMPILE" | "E_CONFIG" | "E_CONTRACT_ERROR" | "E_CONTROL_CHANNEL" | "E_CONTROL_ENVELOPE" | "E_ENGINE_TOO_OLD" | "E_ENTRYPOINT_MISSING" | "E_FIRST_COMPILE" | "E_INPUT_VALIDATION" | "E_INTERNAL" | "E_INVALID_BUNDLE_ID" | "E_LOGS_MISSING" | "E_MANIFEST_BLOCKING" | "E_MANIFEST_NOT_FOUND" | "E_NAVIGATE_ERROR" | "E_NAVIGATE_FAILED" | "E_NETWORK" | "E_NO_CONTROL_CHANNEL" | "E_NO_DIST" | "E_NO_EXTENSION_ID" | "E_NO_MATCHING_TARGET" | "E_NO_REFERENCE_TEMPLATE" | "E_NO_SESSION" | "E_NO_SURFACE_DOCUMENT" | "E_NO_TARGET" | "E_NOT_ATTACHED" | "E_PARSE" | "E_PLATFORM" | "E_PREVIEW_HOST_UNREACHABLE" | "E_PROFILE_LOCKED" | "E_RDP" | "E_SAFARI_ONLY_OPTION" | "E_EVAL" | "E_SCAFFOLD_FAILED" | "E_SCAFFOLD_INCOMPLETE" | "E_SESSION_EXISTS" | "E_SESSION_EXITED" | "E_STALE_CONTRACT" | "E_SURFACE_DID_NOT_OPEN" | "E_TEMPLATE_FETCH" | "E_TEMPLATE_NOT_FOUND" | "E_TOOL_DISABLED" | "E_UNKNOWN_COMMAND" | "E_UNKNOWN_TOOL" | "E_UNSUPPORTED_BROWSER" | "E_WAIT_TIMEOUT";
4
4
  export declare const ERROR_CODES: ErrorCode[];
5
5
  export interface EnvelopeError {
6
6
  code: ErrorCode | string;
@@ -0,0 +1,39 @@
1
+ export type FeatureGroup = "local" | "platform";
2
+ export declare const FEATURE_GROUPS: FeatureGroup[];
3
+ export declare const FEATURES_ENV = "EXTENSION_DEV_FEATURES";
4
+ export declare const NO_SHIP_ENV = "EXTENSION_DEV_NO_SHIP";
5
+ export interface ToolAnnotations {
6
+ readOnlyHint: boolean;
7
+ destructiveHint: boolean;
8
+ idempotentHint: boolean;
9
+ openWorldHint: boolean;
10
+ }
11
+ type Args = Record<string, unknown>;
12
+ export interface ToolPolicy {
13
+ group: FeatureGroup;
14
+ annotations: ToolAnnotations;
15
+ ships?: "always" | ((args: Args) => boolean);
16
+ }
17
+ export declare const TOOL_POLICY: Record<string, ToolPolicy>;
18
+ export interface ServerOptions {
19
+ features: FeatureGroup[];
20
+ noShip: boolean;
21
+ project?: string;
22
+ }
23
+ export declare const DEFAULT_SERVER_OPTIONS: ServerOptions;
24
+ export declare function isServerFlag(arg: string): boolean;
25
+ export declare function resolveServerOptions(argv: string[], env: Record<string, string | undefined>): {
26
+ ok: true;
27
+ options: ServerOptions;
28
+ } | {
29
+ ok: false;
30
+ message: string;
31
+ };
32
+ export declare function isToolListed(name: string, options: ServerOptions): boolean;
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
+ };
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.2",
4
+ "version": "10.10.4",
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",
@@ -91,9 +96,9 @@
91
96
  "@modelcontextprotocol/sdk": "^1.12.1",
92
97
  "browser-extension-manifest-fields": "^2.2.9",
93
98
  "cross-spawn": "^7.0.6",
94
- "extension-create": "4.1.31-canary.1791055414.4bbb683a",
95
- "extension-develop": "4.1.31-canary.1791055414.4bbb683a",
96
- "extension-install": "4.1.31-canary.1791055414.4bbb683a",
99
+ "extension-create": "4.1.31",
100
+ "extension-develop": "4.1.31",
101
+ "extension-install": "4.1.31",
97
102
  "ws": "^8.20.0"
98
103
  },
99
104
  "devDependencies": {
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "10.10.2",
10
+ "version": "10.10.4",
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.2",
16
+ "version": "10.10.4",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"