@extension.dev/mcp 10.10.2 → 10.10.3

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.3",
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.3",
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.3
4
+
5
+ - The engine pin moves off the canary to Extension.js `4.1.31` stable:
6
+ `extension_dev`, `extension_start` and `extension_build` spawn that
7
+ release when a project has no engine of its own, and the nightly
8
+ engine-pin check reads it against `latest` again.
9
+
10
+ - A human now approves a real store submission and a promotion to stable
11
+ before either runs, by default. The first call answers
12
+ `approval-required` with an `approvalUrl` on extension.dev
13
+ (`/device/approve/<id>`) and an `approvalId`; once a workspace member
14
+ approves (an owner, for a submission), the same call with that
15
+ `approvalId` runs exactly that action, once. Promotions to other channels
16
+ and share revokes stay ungated unless `EXTENSION_DEV_APPROVAL_GATE=1`;
17
+ `EXTENSION_DEV_APPROVAL_GATE=0` turns the default off. Whatever the
18
+ setting, a platform that answers `APPROVAL_REQUIRED` now gets the same
19
+ approval flow instead of a bare failure.
20
+
21
+ - Two server flags narrow what an agent can reach. `--features=local`
22
+ or `--features=platform` (env `EXTENSION_DEV_FEATURES`) lists one tool
23
+ group; `local` alone is about 40% smaller. `--no-ship` (env
24
+ `EXTENSION_DEV_NO_SHIP`) hides `extension_publish` and
25
+ `extension_release_promote` and refuses a real submit, a share link and
26
+ a share revoke, while dry runs and listing keep working. A refused call
27
+ answers the new code `E_TOOL_DISABLED` with the flag to change. Both
28
+ default to everything on, so no existing setup changes.
29
+ - Every tool now carries MCP annotations (`readOnlyHint`,
30
+ `destructiveHint`, `idempotentHint`, `openWorldHint`) from one policy
31
+ table, so clients can auto-approve reads and ask before the rest. A
32
+ merged tool takes its worst action: `extension_shares` is destructive
33
+ because revoke is.
34
+ - A device-flow step waiting on a human (`extension_auth` login,
35
+ `extension_project_create`, `extension_workspace_create`) now answers
36
+ `ok: true` with `status: "authorization-pending"` and no error, instead
37
+ of `ok: false` with `E_AUTH_PENDING`, so clients stop rendering the
38
+ first login step as a failure. The approval link and code stay in
39
+ `hint` and `value`; `E_AUTH_PENDING` is retired.
40
+
3
41
  ## 10.10.2
4
42
 
5
43
  - The engine pin moves to the Extension.js canary
package/README.md CHANGED
@@ -75,6 +75,30 @@ Or install it as a plugin, the MCP server plus the `/extension`, `/extension-add
75
75
  }
76
76
  ```
77
77
 
78
+ ### Choosing what the agent can reach
79
+
80
+ Two flags (or environment variables) narrow the server before an agent sees it:
81
+
82
+ - `--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`.
83
+ - `--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`.
84
+
85
+ A refused call answers `E_TOOL_DISABLED` with the flag to change.
86
+
87
+ 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.
88
+
89
+ Every tool also carries MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), so clients can auto-approve reads and ask before the rest.
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "extension-dev": {
95
+ "command": "npx",
96
+ "args": ["@extension.dev/mcp", "--no-ship"]
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
78
102
  ### Pair with the skill
79
103
 
80
104
  This server gives agents hands; [`@extension.dev/skill`](https://www.npmjs.com/package/@extension.dev/skill) gives them judgment: the cross-browser rules, silent-failure gotchas, debugging playbooks, and store checklist, packaged in the open [Agent Skills](https://agentskills.io) format. With both installed, agents know to verify against the live browser instead of guessing, and these tools make that a one-call operation.
@@ -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,28 @@ 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
25
30
  `
26
31
 
27
32
  const [, , cmd, ...rest] = process.argv
28
33
 
29
- if (cmd === undefined) {
30
- startServer()
34
+ if (cmd === undefined || isServerFlag(cmd)) {
35
+ const resolved = resolveServerOptions(process.argv.slice(2), process.env)
36
+ if (!resolved.ok) {
37
+ process.stderr.write(`${resolved.message}\n\n${usage}`)
38
+ process.exit(1)
39
+ }
40
+ startServer(resolved.options)
31
41
  } else if (['login', 'logout', 'whoami', 'release'].includes(cmd)) {
32
42
  runCli(cmd, rest)
33
43
  .then((code) => process.exit(code))
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.3","El":{"OP":"4.1.31"}}');
240
240
  const ENVELOPE_SCHEMA = 1;
241
241
  const collectWarnings = (warnings)=>{
242
242
  if (!warnings) return [];
@@ -1986,13 +1986,9 @@ function pendingEnvelope(start) {
1986
1986
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
1987
1987
  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
1988
  return envelope_envelope({
1989
- ok: false,
1989
+ ok: true,
1990
1990
  command: project_create_COMMAND,
1991
1991
  status: "authorization-pending",
1992
- error: {
1993
- code: "E_AUTH_PENDING",
1994
- message
1995
- },
1996
1992
  value: {
1997
1993
  userCode: start.userCode,
1998
1994
  verificationUri: start.verificationUri,
@@ -2131,17 +2127,14 @@ async function project_create_handler(args) {
2131
2127
  async function finishFromPoll(poll, ctx) {
2132
2128
  if (!poll.ok) {
2133
2129
  if ("pending" === poll.reason) return envelope_envelope({
2134
- ok: false,
2130
+ ok: true,
2135
2131
  command: project_create_COMMAND,
2136
2132
  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
2133
  value: {
2142
2134
  verificationUri: ctx.verificationUri,
2143
2135
  deviceCode: ctx.deviceCode
2144
- }
2136
+ },
2137
+ hint: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call extension_project_create again with this same deviceCode.`
2145
2138
  });
2146
2139
  if ("denied" === poll.reason) return fail("CreateDenied", "Creating the project was denied at extension.dev/device.", "create-denied", "E_AUTH_DENIED");
2147
2140
  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 +2325,9 @@ function workspace_create_pendingEnvelope(start) {
2332
2325
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
2333
2326
  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
2327
  return envelope_envelope({
2335
- ok: false,
2328
+ ok: true,
2336
2329
  command: workspace_create_COMMAND,
2337
2330
  status: "authorization-pending",
2338
- error: {
2339
- code: "E_AUTH_PENDING",
2340
- message
2341
- },
2342
2331
  value: {
2343
2332
  userCode: start.userCode,
2344
2333
  verificationUri: start.verificationUri,
@@ -2419,17 +2408,14 @@ async function workspace_create_handler(args) {
2419
2408
  async function workspace_create_finishFromPoll(poll, ctx) {
2420
2409
  if (!poll.ok) {
2421
2410
  if ("pending" === poll.reason) return envelope_envelope({
2422
- ok: false,
2411
+ ok: true,
2423
2412
  command: workspace_create_COMMAND,
2424
2413
  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
2414
  value: {
2430
2415
  verificationUri: ctx.verificationUri,
2431
2416
  deviceCode: ctx.deviceCode
2432
- }
2417
+ },
2418
+ hint: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call ${workspace_create_COMMAND} again with this same deviceCode.`
2433
2419
  });
2434
2420
  if ("denied" === poll.reason) return workspace_create_fail("CreateDenied", "Creating the workspace was denied at extension.dev/device.", "create-denied", "E_AUTH_DENIED");
2435
2421
  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");
@@ -8204,6 +8190,9 @@ async function revokeArtifact(options) {
8204
8190
  error: {
8205
8191
  name: "SharesRevokeError",
8206
8192
  status: res.status,
8193
+ ..."string" == typeof data?.code ? {
8194
+ code: data.code
8195
+ } : {},
8207
8196
  message: `Revoking ${options.artifactId} failed (${res.status}): ${data?.message || "unknown error"}`
8208
8197
  }
8209
8198
  };
@@ -9176,9 +9165,29 @@ const APPROVAL_GATE_ENV = "EXTENSION_DEV_APPROVAL_GATE";
9176
9165
  const APPROVAL_REQUIRED_STATUS = "approval-required";
9177
9166
  const APPROVAL_PENDING_STATUS = "approval-pending";
9178
9167
  const APPROVAL_REJECTED_STATUS = "approval-rejected";
9179
- function approvalGateEnabled() {
9168
+ function approvalGateEnabled(defaultOn = false) {
9180
9169
  const raw = String(process.env[APPROVAL_GATE_ENV] || "").trim().toLowerCase();
9181
- return "" !== raw && "0" !== raw && "false" !== raw && "off" !== raw;
9170
+ if ("" === raw) return defaultOn;
9171
+ return "0" !== raw && "false" !== raw && "off" !== raw;
9172
+ }
9173
+ function platformRequiresApproval(res, data) {
9174
+ return 403 === res.status && !!data && "object" == typeof data && "APPROVAL_REQUIRED" === data.code;
9175
+ }
9176
+ async function requestApprovalAfterRefusal(input) {
9177
+ const gate = await evaluateApproval({
9178
+ ...input,
9179
+ approvalId: void 0,
9180
+ enabled: true
9181
+ });
9182
+ return gate.blocked ? gate.envelope : envelope_envelope({
9183
+ ok: false,
9184
+ command: input.command,
9185
+ status: APPROVAL_REQUIRED_STATUS,
9186
+ error: {
9187
+ code: "E_APPROVAL_REQUIRED",
9188
+ message: "The platform requires an approval for this action."
9189
+ }
9190
+ });
9182
9191
  }
9183
9192
  function canonicalScope(scope) {
9184
9193
  return Object.keys(scope).sort().map((key)=>{
@@ -9666,18 +9675,19 @@ async function revokeShare(args) {
9666
9675
  const token = resolveToken({
9667
9676
  project: args.project
9668
9677
  });
9678
+ const gateInput = {
9679
+ command: "extension_shares",
9680
+ action: "extension_shares.revoke",
9681
+ scope: {
9682
+ artifactId: ref
9683
+ },
9684
+ description: `Permanently revoke share ${ref}, killing the link for everyone with no way to restore it.`,
9685
+ approvalId: args.approvalId,
9686
+ token: token || "",
9687
+ api: args.api
9688
+ };
9669
9689
  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
- });
9690
+ const gate = await evaluateApproval(gateInput);
9681
9691
  if (gate.blocked) return gate.envelope;
9682
9692
  }
9683
9693
  const result = await revokeArtifact({
@@ -9703,6 +9713,7 @@ async function revokeShare(args) {
9703
9713
  artifactId: ref
9704
9714
  }
9705
9715
  });
9716
+ if (token && !args.approvalId && 403 === result.error.status && "APPROVAL_REQUIRED" === result.error.code) return requestApprovalAfterRefusal(gateInput);
9706
9717
  const isAuth = "SharesAuthError" === result.error.name;
9707
9718
  return envelope_envelope({
9708
9719
  ok: false,
@@ -15393,7 +15404,7 @@ async function release_promote_handler(args) {
15393
15404
  const url = `${apiCheck.base}/api/cli/release/promote`;
15394
15405
  const sourceChannel = args.sourceChannel ? String(args.sourceChannel).trim() : "";
15395
15406
  const gateBrowsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim()).filter(Boolean);
15396
- const gate = await evaluateApproval({
15407
+ const gateInput = {
15397
15408
  command: "extension_release_promote",
15398
15409
  action: "extension_release_promote",
15399
15410
  scope: {
@@ -15412,6 +15423,10 @@ async function release_promote_handler(args) {
15412
15423
  approvalId: args.approvalId,
15413
15424
  token,
15414
15425
  api: args.api
15426
+ };
15427
+ const gate = await evaluateApproval({
15428
+ ...gateInput,
15429
+ enabled: approvalGateEnabled("stable" === channel)
15415
15430
  });
15416
15431
  if (gate.blocked) return gate.envelope;
15417
15432
  const body = {
@@ -15456,6 +15471,7 @@ async function release_promote_handler(args) {
15456
15471
  buildId
15457
15472
  }
15458
15473
  });
15474
+ if (!args.approvalId && platformRequiresApproval(res, data)) return requestApprovalAfterRefusal(gateInput);
15459
15475
  const code = "string" == typeof data?.code ? data.code : void 0;
15460
15476
  const enrich = {};
15461
15477
  let hint = "";
@@ -16090,21 +16106,25 @@ async function submit_handler(args) {
16090
16106
  };
16091
16107
  if (args.channel) body.channel = String(args.channel).trim();
16092
16108
  if (args.version) body.version = String(args.version).trim();
16109
+ const gateInput = {
16110
+ command: "extension_submit",
16111
+ action: "extension_submit",
16112
+ scope: {
16113
+ browsers: [
16114
+ ...browsers
16115
+ ].sort(),
16116
+ buildSha: buildSha.toLowerCase(),
16117
+ channel
16118
+ },
16119
+ description: `Submit build ${buildSha} to ${browsers.join(", ")} for store review.`,
16120
+ approvalId: args.approvalId,
16121
+ token,
16122
+ api: args.api
16123
+ };
16093
16124
  if (!dryRun) {
16094
16125
  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
16126
+ ...gateInput,
16127
+ enabled: approvalGateEnabled(true)
16108
16128
  });
16109
16129
  if (gate.blocked) return gate.envelope;
16110
16130
  if (gate.approvalId) body.approvalId = gate.approvalId;
@@ -16144,6 +16164,7 @@ async function submit_handler(args) {
16144
16164
  dryRun
16145
16165
  }
16146
16166
  });
16167
+ if (!dryRun && !args.approvalId && platformRequiresApproval(res, data)) return requestApprovalAfterRefusal(gateInput);
16147
16168
  return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`, "submit-failed", "E_PLATFORM");
16148
16169
  }
16149
16170
  const warnings = Array.isArray(data?.warnings) ? [
@@ -16793,13 +16814,9 @@ function login_pending(start) {
16793
16814
  const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
16794
16815
  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
16816
  return envelope_envelope({
16796
- ok: false,
16817
+ ok: true,
16797
16818
  command: "extension_auth",
16798
16819
  status: "authorization-pending",
16799
- error: {
16800
- code: "E_AUTH_PENDING",
16801
- message
16802
- },
16803
16820
  value: {
16804
16821
  userCode: start.userCode,
16805
16822
  verificationUri: start.verificationUri,
@@ -16818,13 +16835,9 @@ function login_pending(start) {
16818
16835
  function resumePending(deviceCode, verificationUri) {
16819
16836
  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
16837
  return envelope_envelope({
16821
- ok: false,
16838
+ ok: true,
16822
16839
  command: "extension_auth",
16823
16840
  status: "authorization-pending",
16824
- error: {
16825
- code: "E_AUTH_PENDING",
16826
- message
16827
- },
16828
16841
  value: {
16829
16842
  verificationUri,
16830
16843
  deviceCode,
@@ -17843,6 +17856,288 @@ function inputValidationError(toolName, issues, inputSchema) {
17843
17856
  }
17844
17857
  });
17845
17858
  }
17859
+ const FEATURE_GROUPS = [
17860
+ "local",
17861
+ "platform"
17862
+ ];
17863
+ const FEATURES_ENV = "EXTENSION_DEV_FEATURES";
17864
+ const NO_SHIP_ENV = "EXTENSION_DEV_NO_SHIP";
17865
+ const reads = (openWorld = false)=>({
17866
+ readOnlyHint: true,
17867
+ destructiveHint: false,
17868
+ idempotentHint: true,
17869
+ openWorldHint: openWorld
17870
+ });
17871
+ const acts = (over = {})=>({
17872
+ readOnlyHint: false,
17873
+ destructiveHint: false,
17874
+ idempotentHint: false,
17875
+ openWorldHint: false,
17876
+ ...over
17877
+ });
17878
+ const TOOL_POLICY = {
17879
+ extension_create: {
17880
+ group: "local",
17881
+ annotations: acts({
17882
+ openWorldHint: true
17883
+ })
17884
+ },
17885
+ extension_templates: {
17886
+ group: "local",
17887
+ annotations: reads(true)
17888
+ },
17889
+ extension_add_feature: {
17890
+ group: "local",
17891
+ annotations: reads()
17892
+ },
17893
+ extension_build: {
17894
+ group: "local",
17895
+ annotations: acts({
17896
+ idempotentHint: true
17897
+ })
17898
+ },
17899
+ extension_dev: {
17900
+ group: "local",
17901
+ annotations: acts()
17902
+ },
17903
+ extension_start: {
17904
+ group: "local",
17905
+ annotations: acts()
17906
+ },
17907
+ extension_wait: {
17908
+ group: "local",
17909
+ annotations: reads()
17910
+ },
17911
+ extension_stop: {
17912
+ group: "local",
17913
+ annotations: acts({
17914
+ idempotentHint: true
17915
+ })
17916
+ },
17917
+ extension_manifest_validate: {
17918
+ group: "local",
17919
+ annotations: reads()
17920
+ },
17921
+ extension_theme_verify: {
17922
+ group: "local",
17923
+ annotations: reads()
17924
+ },
17925
+ extension_analyze: {
17926
+ group: "local",
17927
+ annotations: reads()
17928
+ },
17929
+ extension_assert: {
17930
+ group: "local",
17931
+ annotations: acts({
17932
+ idempotentHint: true
17933
+ })
17934
+ },
17935
+ extension_inspect: {
17936
+ group: "local",
17937
+ annotations: acts({
17938
+ idempotentHint: true
17939
+ })
17940
+ },
17941
+ extension_dom_snapshot: {
17942
+ group: "local",
17943
+ annotations: reads()
17944
+ },
17945
+ extension_list_extensions: {
17946
+ group: "local",
17947
+ annotations: reads()
17948
+ },
17949
+ extension_logs: {
17950
+ group: "local",
17951
+ annotations: reads()
17952
+ },
17953
+ extension_doctor: {
17954
+ group: "local",
17955
+ annotations: reads()
17956
+ },
17957
+ extension_eval: {
17958
+ group: "local",
17959
+ annotations: acts({
17960
+ destructiveHint: true,
17961
+ openWorldHint: true
17962
+ })
17963
+ },
17964
+ extension_storage: {
17965
+ group: "local",
17966
+ annotations: acts({
17967
+ destructiveHint: true
17968
+ })
17969
+ },
17970
+ extension_reload: {
17971
+ group: "local",
17972
+ annotations: acts({
17973
+ idempotentHint: true
17974
+ })
17975
+ },
17976
+ extension_open: {
17977
+ group: "local",
17978
+ annotations: acts({
17979
+ openWorldHint: true
17980
+ })
17981
+ },
17982
+ extension_browsers: {
17983
+ group: "local",
17984
+ annotations: acts({
17985
+ destructiveHint: true,
17986
+ openWorldHint: true
17987
+ })
17988
+ },
17989
+ extension_auth: {
17990
+ group: "platform",
17991
+ annotations: acts({
17992
+ openWorldHint: true
17993
+ })
17994
+ },
17995
+ extension_workspace_create: {
17996
+ group: "platform",
17997
+ annotations: acts({
17998
+ openWorldHint: true
17999
+ })
18000
+ },
18001
+ extension_project_create: {
18002
+ group: "platform",
18003
+ annotations: acts({
18004
+ openWorldHint: true
18005
+ })
18006
+ },
18007
+ extension_preview_web: {
18008
+ group: "platform",
18009
+ annotations: acts({
18010
+ openWorldHint: true
18011
+ }),
18012
+ ships: (args)=>true === args.share
18013
+ },
18014
+ extension_shares: {
18015
+ group: "platform",
18016
+ annotations: acts({
18017
+ destructiveHint: true,
18018
+ openWorldHint: true
18019
+ }),
18020
+ ships: (args)=>"revoke" === args.action
18021
+ },
18022
+ extension_publish: {
18023
+ group: "platform",
18024
+ annotations: acts({
18025
+ openWorldHint: true
18026
+ }),
18027
+ ships: "always"
18028
+ },
18029
+ extension_release_status: {
18030
+ group: "platform",
18031
+ annotations: reads(true)
18032
+ },
18033
+ extension_release_promote: {
18034
+ group: "platform",
18035
+ annotations: acts({
18036
+ destructiveHint: true,
18037
+ openWorldHint: true
18038
+ }),
18039
+ ships: "always"
18040
+ },
18041
+ extension_submit: {
18042
+ group: "platform",
18043
+ annotations: acts({
18044
+ destructiveHint: true,
18045
+ openWorldHint: true
18046
+ }),
18047
+ ships: (args)=>false === args.dryRun
18048
+ }
18049
+ };
18050
+ const DEFAULT_SERVER_OPTIONS = {
18051
+ features: [
18052
+ ...FEATURE_GROUPS
18053
+ ],
18054
+ noShip: false
18055
+ };
18056
+ const truthy = (raw)=>{
18057
+ const value = String(raw ?? "").trim().toLowerCase();
18058
+ return "" !== value && "0" !== value && "false" !== value && "off" !== value;
18059
+ };
18060
+ function isServerFlag(arg) {
18061
+ return "--no-ship" === arg || "--features" === arg || arg.startsWith("--features=");
18062
+ }
18063
+ function resolveServerOptions(argv, env) {
18064
+ let rawFeatures = env[FEATURES_ENV];
18065
+ let noShip = truthy(env[NO_SHIP_ENV]);
18066
+ for(let i = 0; i < argv.length; i++){
18067
+ const arg = argv[i];
18068
+ if ("--no-ship" === arg) noShip = true;
18069
+ else if ("--features" === arg) rawFeatures = argv[++i] ?? "";
18070
+ else {
18071
+ if (!arg.startsWith("--features=")) return {
18072
+ ok: false,
18073
+ message: `Unknown flag "${arg}".`
18074
+ };
18075
+ rawFeatures = arg.slice(11);
18076
+ }
18077
+ }
18078
+ if (void 0 === rawFeatures || "" === rawFeatures.trim()) return {
18079
+ ok: true,
18080
+ options: {
18081
+ features: [
18082
+ ...FEATURE_GROUPS
18083
+ ],
18084
+ noShip
18085
+ }
18086
+ };
18087
+ const names = rawFeatures.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);
18088
+ const unknown = names.filter((name)=>!FEATURE_GROUPS.includes(name));
18089
+ if (unknown.length) return {
18090
+ ok: false,
18091
+ message: `Unknown feature group ${unknown.map((n)=>`"${n}"`).join(", ")}. Use ${FEATURE_GROUPS.join(" or ")}, comma-separated.`
18092
+ };
18093
+ const features = FEATURE_GROUPS.filter((group)=>names.includes(group));
18094
+ return {
18095
+ ok: true,
18096
+ options: {
18097
+ features,
18098
+ noShip
18099
+ }
18100
+ };
18101
+ }
18102
+ function isToolListed(name, options) {
18103
+ const policy = TOOL_POLICY[name];
18104
+ if (!policy) return false;
18105
+ if (!options.features.includes(policy.group)) return false;
18106
+ return !(options.noShip && "always" === policy.ships);
18107
+ }
18108
+ function disabledToolEnvelope(name, args, options) {
18109
+ const policy = TOOL_POLICY[name];
18110
+ if (!policy) return null;
18111
+ if (!options.features.includes(policy.group)) return envelope_envelope({
18112
+ ok: false,
18113
+ command: name,
18114
+ status: "tool-disabled",
18115
+ error: {
18116
+ code: "E_TOOL_DISABLED",
18117
+ message: `${name} is in the "${policy.group}" feature group, which this server was started without.`
18118
+ },
18119
+ value: {
18120
+ group: policy.group,
18121
+ features: options.features
18122
+ },
18123
+ hint: `Add ${policy.group} to --features (or ${FEATURES_ENV}) in this server's MCP config, or ask the user to.`
18124
+ });
18125
+ const ships = "always" === policy.ships || "function" == typeof policy.ships && policy.ships(args);
18126
+ if (options.noShip && ships) return envelope_envelope({
18127
+ ok: false,
18128
+ command: name,
18129
+ status: "tool-disabled",
18130
+ error: {
18131
+ code: "E_TOOL_DISABLED",
18132
+ message: `${name} with these arguments reaches people outside this machine, and this server was started in no-ship mode.`
18133
+ },
18134
+ value: {
18135
+ noShip: true
18136
+ },
18137
+ 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.`
18138
+ });
18139
+ return null;
18140
+ }
17846
18141
  const tools = [
17847
18142
  create_namespaceObject,
17848
18143
  templates_namespaceObject,
@@ -17898,8 +18193,8 @@ function toolResultFrame(result) {
17898
18193
  }
17899
18194
  const toolMap = new Map();
17900
18195
  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() {
18196
+ 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.";
18197
+ function createServer(options = DEFAULT_SERVER_OPTIONS) {
17903
18198
  const server = new Server({
17904
18199
  name: "extension-dev",
17905
18200
  version: package_namespaceObject.rE
@@ -17910,10 +18205,11 @@ function createServer() {
17910
18205
  instructions: SERVER_INSTRUCTIONS
17911
18206
  });
17912
18207
  server.setRequestHandler(ListToolsRequestSchema, async ()=>({
17913
- tools: tools.map((t)=>({
18208
+ tools: tools.filter((t)=>isToolListed(t.schema.name, options)).map((t)=>({
17914
18209
  name: t.schema.name,
17915
18210
  description: t.schema.description,
17916
- inputSchema: t.schema.inputSchema
18211
+ inputSchema: t.schema.inputSchema,
18212
+ annotations: TOOL_POLICY[t.schema.name].annotations
17917
18213
  }))
17918
18214
  }));
17919
18215
  server.setRequestHandler(CallToolRequestSchema, async (request)=>{
@@ -17932,7 +18228,7 @@ function createServer() {
17932
18228
  message: `Unknown tool: ${name}`
17933
18229
  },
17934
18230
  value: {
17935
- availableTools: tools.map((t)=>t.schema.name)
18231
+ availableTools: tools.map((t)=>t.schema.name).filter((toolName)=>isToolListed(toolName, options))
17936
18232
  }
17937
18233
  })
17938
18234
  }
@@ -17940,6 +18236,16 @@ function createServer() {
17940
18236
  isError: true
17941
18237
  };
17942
18238
  const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
18239
+ const disabled = disabledToolEnvelope(name, normalizedArgs, options);
18240
+ if (disabled) return {
18241
+ content: [
18242
+ {
18243
+ type: "text",
18244
+ text: disabled
18245
+ }
18246
+ ],
18247
+ isError: true
18248
+ };
17943
18249
  const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
17944
18250
  if (issues.length) return {
17945
18251
  content: [
@@ -17976,9 +18282,9 @@ function createServer() {
17976
18282
  });
17977
18283
  return server;
17978
18284
  }
17979
- async function startServer() {
18285
+ async function startServer(options = DEFAULT_SERVER_OPTIONS) {
17980
18286
  installCarrierExitCleanup();
17981
- const server = createServer();
18287
+ const server = createServer(options);
17982
18288
  const transport = new StdioServerTransport();
17983
18289
  await server.connect(transport);
17984
18290
  }
@@ -18074,4 +18380,4 @@ async function runCli(cmd, args) {
18074
18380
  return 1;
18075
18381
  }
18076
18382
  var version_0 = package_namespaceObject.rE;
18077
- export { runCli, startServer, version_0 as version };
18383
+ export { isServerFlag, resolveServerOptions, runCli, startServer, version_0 as version };
@@ -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;
@@ -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,33 @@
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
+ }
22
+ export declare const DEFAULT_SERVER_OPTIONS: ServerOptions;
23
+ export declare function isServerFlag(arg: string): boolean;
24
+ export declare function resolveServerOptions(argv: string[], env: Record<string, string | undefined>): {
25
+ ok: true;
26
+ options: ServerOptions;
27
+ } | {
28
+ ok: false;
29
+ message: string;
30
+ };
31
+ export declare function isToolListed(name: string, options: ServerOptions): boolean;
32
+ export declare function disabledToolEnvelope(name: string, args: Args, options: ServerOptions): string | null;
33
+ 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.3",
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",
@@ -91,9 +91,9 @@
91
91
  "@modelcontextprotocol/sdk": "^1.12.1",
92
92
  "browser-extension-manifest-fields": "^2.2.9",
93
93
  "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",
94
+ "extension-create": "4.1.31",
95
+ "extension-develop": "4.1.31",
96
+ "extension-install": "4.1.31",
97
97
  "ws": "^8.20.0"
98
98
  },
99
99
  "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.3",
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.3",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"