@extension.dev/mcp 10.7.0 → 10.8.0

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.7.0",
13
+ "version": "10.8.0",
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.7.0",
4
+ "version": "10.8.0",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 10.8.0
4
+
5
+ Some MCP actions change what a public channel serves or hand something to a
6
+ store, and none of those can be taken back in place. The client now carries
7
+ an approval gate for exactly those actions, so a human can stand between an
8
+ agent's proposal and the write when the platform asks for one.
9
+
10
+ - `extension_submit`, `extension_release_promote` and the destructive
11
+ `extension_shares` actions accept an `approvalId`. When the platform's
12
+ approval gate is on, the first call answers `approval-required` with an
13
+ approval id and a URL a human approves at extension.dev; the same call
14
+ repeated with that id performs the action. Rejections and pending
15
+ approvals answer as themselves, never as a bare error.
16
+ - Approvals are bound to an action fingerprint, so an approval for one
17
+ promote cannot be replayed on another.
18
+ - The gate is off unless the platform enables it; every existing flow is
19
+ unchanged by default.
20
+
3
21
  ## 10.7.0
4
22
 
5
23
  The published client had no idea the platform could be held, so on the five
package/dist/module.js CHANGED
@@ -222,7 +222,7 @@ __webpack_require__.d(wait_namespaceObject, {
222
222
  handler: ()=>wait_handler,
223
223
  schema: ()=>wait_schema
224
224
  });
225
- var package_namespaceObject = JSON.parse('{"rE":"10.7.0","El":{"OP":"4.0.30"}}');
225
+ var package_namespaceObject = JSON.parse('{"rE":"10.8.0","El":{"OP":"4.0.30"}}');
226
226
  const ENVELOPE_SCHEMA = 1;
227
227
  const collectWarnings = (warnings)=>{
228
228
  if (!warnings) return [];
@@ -1080,7 +1080,7 @@ function parseBuildIndex(json) {
1080
1080
  }
1081
1081
  const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
1082
1082
  const DEFAULT_CHANNEL = "latest";
1083
- const PINNED_COMMIT = "f7f4e6efb56a7e5ae08d58dbff3972d94af7d021";
1083
+ const PINNED_COMMIT = "1d04a3d62f4d82ab3f13a3388800d517d1627707";
1084
1084
  const CHANNEL_CACHE_TTL_MS = 300000;
1085
1085
  function mediaOrigin() {
1086
1086
  return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
@@ -1144,6 +1144,10 @@ function templateCatalogUrl(slug, utmSource = "mcp") {
1144
1144
  const origin = mcpOrigins().templates.replace(/\/+$/, "");
1145
1145
  return `${origin}/${encodeURIComponent(slug)}?utm_source=${utmSource}&utm_medium=tool`;
1146
1146
  }
1147
+ async function resolvedTemplateCommit() {
1148
+ const release = await resolveRelease();
1149
+ return release?.commit || PINNED_COMMIT;
1150
+ }
1147
1151
  async function templateMetaUrls() {
1148
1152
  const urls = [];
1149
1153
  const release = await resolveRelease();
@@ -1169,6 +1173,86 @@ async function templateFileUrls(slug, relativePath) {
1169
1173
  urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/examples/${slug}/${relative}`);
1170
1174
  return urls;
1171
1175
  }
1176
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
1177
+ const PLATFORM_PROJECT_KEY = "phc_t8hwHt3uJdjxil8TUA9AIWUFeWyJtTxhfXV58bPiV6T";
1178
+ const DRAFT_SEEDED_EVENT = "draft_seeded";
1179
+ const FUNNEL_ENTRY = "mcp";
1180
+ const FUNNEL_SOURCE = "@extension.dev/mcp";
1181
+ const FUNNEL_EMITTED_FROM = "node";
1182
+ function posthogHost() {
1183
+ return String(process.env.EXTENSION_DEV_POSTHOG_HOST || "").trim() || DEFAULT_POSTHOG_HOST;
1184
+ }
1185
+ function trimSlashes(value) {
1186
+ return String(value || "").replace(/\/+$/, "");
1187
+ }
1188
+ function funnelEnvironment() {
1189
+ return trimSlashes(mcpOrigins().www) === trimSlashes(PROD_ORIGINS.www) ? "production" : "development";
1190
+ }
1191
+ function posthogKeyForEnvironment(environment) {
1192
+ if ("production" === String(environment || "").trim()) return String(process.env.EXTENSION_DEV_POSTHOG_KEY || "").trim() || PLATFORM_PROJECT_KEY;
1193
+ return String(process.env.EXTENSION_DEV_POSTHOG_KEY_NONPRODUCTION || "").trim();
1194
+ }
1195
+ function shouldSendCreationFunnelEvent() {
1196
+ if (telemetryDisabled()) return false;
1197
+ if (!posthogKeyForEnvironment(funnelEnvironment())) return false;
1198
+ if (!session_identity_sessionId()) return false;
1199
+ return "function" == typeof fetch;
1200
+ }
1201
+ function seedRef(slug, commit) {
1202
+ return `${String(slug || "").trim()}@${String(commit || "").trim()}`;
1203
+ }
1204
+ function creationFunnelPayload(event, properties, now = new Date()) {
1205
+ if (!shouldSendCreationFunnelEvent()) return null;
1206
+ const environment = funnelEnvironment();
1207
+ const session = session_identity_sessionId();
1208
+ return {
1209
+ api_key: posthogKeyForEnvironment(environment),
1210
+ event,
1211
+ distinct_id: session,
1212
+ timestamp: now.toISOString(),
1213
+ properties: {
1214
+ draft_id: null,
1215
+ ...properties,
1216
+ source: FUNNEL_SOURCE,
1217
+ entry: FUNNEL_ENTRY,
1218
+ session_id: session,
1219
+ environment,
1220
+ emitted_from: FUNNEL_EMITTED_FROM,
1221
+ $process_person_profile: false
1222
+ }
1223
+ };
1224
+ }
1225
+ async function captureCreationFunnelEvent(event, properties, fetchImpl = fetch) {
1226
+ try {
1227
+ const payload = creationFunnelPayload(event, properties);
1228
+ if (!payload) return null;
1229
+ await fetchImpl(`${posthogHost()}/capture/`, {
1230
+ method: "POST",
1231
+ headers: {
1232
+ "Content-Type": "application/json"
1233
+ },
1234
+ body: JSON.stringify(payload)
1235
+ }).catch(()=>void 0);
1236
+ return payload;
1237
+ } catch {
1238
+ return null;
1239
+ }
1240
+ }
1241
+ async function captureTemplateSeed(input, fetchImpl = fetch) {
1242
+ try {
1243
+ const slug = String(input?.slug || "").trim();
1244
+ if (!slug) return null;
1245
+ if (!shouldSendCreationFunnelEvent()) return null;
1246
+ const commit = await resolvedTemplateCommit();
1247
+ return await captureCreationFunnelEvent(DRAFT_SEEDED_EVENT, {
1248
+ seed_source: input.source ?? "template",
1249
+ seed_ref: seedRef(slug, commit),
1250
+ seed_slug: slug
1251
+ }, fetchImpl);
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ }
1172
1256
  const COMMAND = "extension_create";
1173
1257
  function scaffoldEnginePin(projectPath) {
1174
1258
  try {
@@ -1323,6 +1407,10 @@ async function create_handler(args) {
1323
1407
  },
1324
1408
  hint: "Delete the directory and retry extension_create; a template download interrupted mid-way can leave a partial tree."
1325
1409
  });
1410
+ captureTemplateSeed({
1411
+ slug: result.template,
1412
+ source: "template"
1413
+ });
1326
1414
  const packageManager = result.packageManager || (result.depsInstalled ? detectPackageManager(result.projectPath) : "npm");
1327
1415
  const runDev = `${packageManager} run dev`;
1328
1416
  const addDev = (spec)=>"npm" === packageManager ? `npm i -D ${spec}` : "yarn" === packageManager ? `yarn add -D ${spec}` : `${packageManager} add -D ${spec}`;
@@ -6199,6 +6287,7 @@ async function revokeArtifact(options) {
6199
6287
  }
6200
6288
  };
6201
6289
  const url = `${apiCheck.base}/api/artifacts/${encodeURIComponent(options.artifactId)}`;
6290
+ const approvalId = String(options.approvalId || "").trim();
6202
6291
  const doFetch = options.fetchImpl ?? fetch;
6203
6292
  let res;
6204
6293
  try {
@@ -6207,6 +6296,9 @@ async function revokeArtifact(options) {
6207
6296
  headers: {
6208
6297
  authorization: `Bearer ${token}`,
6209
6298
  accept: "application/json",
6299
+ ...approvalId ? {
6300
+ "x-extensiondev-approval": approvalId
6301
+ } : {},
6210
6302
  ...identityHeaders("extension_shares")
6211
6303
  }
6212
6304
  });
@@ -6554,6 +6646,7 @@ async function probeShareCors(options) {
6554
6646
  finalStatus: 0,
6555
6647
  redirects,
6556
6648
  allowOrigin: null,
6649
+ held: false,
6557
6650
  ...extra
6558
6651
  });
6559
6652
  for(let hop = 0; hop < MAX_HOPS; hop++){
@@ -6582,6 +6675,13 @@ async function probeShareCors(options) {
6582
6675
  }
6583
6676
  const status = res.status;
6584
6677
  const allowOrigin = res.headers.get("access-control-allow-origin");
6678
+ if (sawPlatformHold(res)) return verdict({
6679
+ ok: false,
6680
+ held: true,
6681
+ finalStatus: status,
6682
+ allowOrigin,
6683
+ reason: `${url} answered ${status} with the platform hold, so this link is held from the public until extension.dev opens. It is not broken: the share was created and will open once the platform is open. A signed-in operator can still reach it.`
6684
+ });
6585
6685
  if (status >= 300 && status < 400) {
6586
6686
  const location = res.headers.get("location");
6587
6687
  if (!location) return verdict({
@@ -6783,7 +6883,11 @@ async function buildShare(projectPath, distDir, manifest, browser, verifyInBrows
6783
6883
  } : {},
6784
6884
  serves: "uploaded-local-build",
6785
6885
  localBuildUploaded: true,
6786
- ...browserCheck ? {
6886
+ ...browserCheck ? browserCheck.held ? {
6887
+ browserLoadable: null,
6888
+ heldFromPublic: true,
6889
+ browserCheck
6890
+ } : {
6787
6891
  browserLoadable: browserCheck.ok,
6788
6892
  browserCheck
6789
6893
  } : {
@@ -7164,6 +7268,216 @@ async function preview_web_handler(args) {
7164
7268
  });
7165
7269
  }
7166
7270
  }
7271
+ const APPROVAL_GATE_ENV = "EXTENSION_DEV_APPROVAL_GATE";
7272
+ const APPROVAL_REQUIRED_STATUS = "approval-required";
7273
+ const APPROVAL_PENDING_STATUS = "approval-pending";
7274
+ const APPROVAL_REJECTED_STATUS = "approval-rejected";
7275
+ function approvalGateEnabled() {
7276
+ const raw = String(process.env[APPROVAL_GATE_ENV] || "").trim().toLowerCase();
7277
+ return "" !== raw && "0" !== raw && "false" !== raw && "off" !== raw;
7278
+ }
7279
+ function canonicalScope(scope) {
7280
+ return Object.keys(scope).sort().map((key)=>{
7281
+ const value = scope[key];
7282
+ const rendered = Array.isArray(value) ? [
7283
+ ...value
7284
+ ].map((v)=>String(v)).sort().join(",") : String(value);
7285
+ return `${key}=${rendered}`;
7286
+ }).join("\n");
7287
+ }
7288
+ function actionFingerprint(action, scope) {
7289
+ return node_crypto.createHash("sha256").update(`${action}\n${canonicalScope(scope)}`).digest("hex");
7290
+ }
7291
+ function approval_gate_block(command, status, code, name, message, value, hint) {
7292
+ return {
7293
+ blocked: true,
7294
+ envelope: envelope_envelope({
7295
+ ok: false,
7296
+ command,
7297
+ status,
7298
+ value,
7299
+ error: {
7300
+ code,
7301
+ name,
7302
+ message
7303
+ },
7304
+ ...hint ? {
7305
+ hint
7306
+ } : {}
7307
+ })
7308
+ };
7309
+ }
7310
+ async function approval_gate_readJson(res) {
7311
+ const text = await res.text();
7312
+ try {
7313
+ const parsed = JSON.parse(text);
7314
+ return parsed && "object" == typeof parsed && !Array.isArray(parsed) ? parsed : {
7315
+ message: text
7316
+ };
7317
+ } catch {
7318
+ return {
7319
+ message: text
7320
+ };
7321
+ }
7322
+ }
7323
+ async function requestApproval(params) {
7324
+ const { base, fingerprint, input, doFetch } = params;
7325
+ const url = `${base}/api/cli/approvals`;
7326
+ let res;
7327
+ try {
7328
+ res = await doFetch(url, {
7329
+ method: "POST",
7330
+ headers: {
7331
+ authorization: `Bearer ${input.token}`,
7332
+ "content-type": "application/json"
7333
+ },
7334
+ body: JSON.stringify({
7335
+ action: input.action,
7336
+ fingerprint,
7337
+ scope: input.scope,
7338
+ description: input.description
7339
+ })
7340
+ });
7341
+ } catch (err) {
7342
+ return approval_gate_block(input.command, APPROVAL_REQUIRED_STATUS, "E_APPROVAL_REQUIRED", "ApprovalRequired", `A human must approve this irreversible action before it runs, and the approval could not be requested from ${url}: ${err?.message || err}. Nothing was executed. Retry once the platform is reachable.`, {
7343
+ action: input.action,
7344
+ fingerprint,
7345
+ description: input.description
7346
+ });
7347
+ }
7348
+ const data = await approval_gate_readJson(res);
7349
+ if (sawPlatformHold(res, data)) return {
7350
+ blocked: true,
7351
+ envelope: platformHoldEnvelope({
7352
+ command: input.command,
7353
+ name: "ApprovalHeld",
7354
+ body: data,
7355
+ api: input.api,
7356
+ value: {
7357
+ action: input.action,
7358
+ description: input.description
7359
+ }
7360
+ })
7361
+ };
7362
+ if (!res.ok) return approval_gate_block(input.command, APPROVAL_REQUIRED_STATUS, "E_APPROVAL_REQUIRED", "ApprovalRequired", `A human must approve this irreversible action before it runs. The platform did not create an approval request (${res.status}): ${"string" == typeof data.message && data.message ? data.message : "no approval endpoint answered"}. Nothing was executed.`, {
7363
+ action: input.action,
7364
+ fingerprint,
7365
+ description: input.description
7366
+ });
7367
+ const approvalId = String(data.approvalId || "").trim();
7368
+ const approvalUrl = String(data.approvalUrl || "").trim();
7369
+ return approval_gate_block(input.command, APPROVAL_REQUIRED_STATUS, "E_APPROVAL_REQUIRED", "ApprovalRequired", `${input.description} This is irreversible, so a human must approve it first. Open ${approvalUrl || "the approval URL"} to review and approve, then call this tool again with approvalId ${approvalId || "<from the response>"}. Nothing was executed.`, {
7370
+ action: input.action,
7371
+ fingerprint,
7372
+ description: input.description,
7373
+ ...approvalId ? {
7374
+ approvalId
7375
+ } : {},
7376
+ ...approvalUrl ? {
7377
+ approvalUrl
7378
+ } : {},
7379
+ ...data.expiresAt ? {
7380
+ expiresAt: data.expiresAt
7381
+ } : {}
7382
+ }, approvalUrl ? `Approve at ${approvalUrl}, then re-run with approvalId ${approvalId}.` : void 0);
7383
+ }
7384
+ async function verifyApproval(params) {
7385
+ const { base, fingerprint, input, doFetch } = params;
7386
+ const approvalId = String(input.approvalId || "").trim();
7387
+ const url = `${base}/api/cli/approvals/${encodeURIComponent(approvalId)}`;
7388
+ let res;
7389
+ try {
7390
+ res = await doFetch(url, {
7391
+ method: "GET",
7392
+ headers: {
7393
+ authorization: `Bearer ${input.token}`,
7394
+ accept: "application/json"
7395
+ }
7396
+ });
7397
+ } catch (err) {
7398
+ return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_APPROVAL_REJECTED", "ApprovalUnverifiable", `The approval ${approvalId} could not be verified with the platform (${err?.message || err}), so this irreversible action was refused. Nothing was executed.`, {
7399
+ action: input.action,
7400
+ approvalId,
7401
+ fingerprint
7402
+ });
7403
+ }
7404
+ const data = await approval_gate_readJson(res);
7405
+ if (sawPlatformHold(res, data)) return {
7406
+ blocked: true,
7407
+ envelope: platformHoldEnvelope({
7408
+ command: input.command,
7409
+ name: "ApprovalHeld",
7410
+ body: data,
7411
+ api: input.api,
7412
+ value: {
7413
+ action: input.action,
7414
+ approvalId
7415
+ }
7416
+ })
7417
+ };
7418
+ if (!res.ok) return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_APPROVAL_REJECTED", "ApprovalUnverifiable", `The platform did not confirm approval ${approvalId} (${res.status}): ${"string" == typeof data.message && data.message ? data.message : "unknown approval"}. This irreversible action was refused. Request a fresh approval by calling this tool with no approvalId.`, {
7419
+ action: input.action,
7420
+ approvalId,
7421
+ fingerprint
7422
+ });
7423
+ const status = String(data.status || "").trim().toLowerCase();
7424
+ const storedFingerprint = String(data.fingerprint || "").trim();
7425
+ const used = true === data.used;
7426
+ const now = input.now ?? Date.now();
7427
+ const expiresAt = "string" == typeof data.expiresAt ? Date.parse(data.expiresAt) : NaN;
7428
+ const expired = "expired" === status || Number.isFinite(expiresAt) && expiresAt <= now;
7429
+ if (storedFingerprint && storedFingerprint !== fingerprint) return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_APPROVAL_REJECTED", "ApprovalScopeMismatch", `Approval ${approvalId} was granted for a different action and cannot authorize ${input.action}: ${input.description} Request a fresh approval for this exact action by calling this tool with no approvalId.`, {
7430
+ action: input.action,
7431
+ approvalId,
7432
+ expectedFingerprint: fingerprint,
7433
+ approvedFingerprint: storedFingerprint
7434
+ });
7435
+ if ("denied" === status) return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_APPROVAL_REJECTED", "ApprovalDenied", `Approval ${approvalId} was denied by the human reviewer, so nothing was executed.`, {
7436
+ action: input.action,
7437
+ approvalId
7438
+ });
7439
+ if (expired) return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_APPROVAL_REJECTED", "ApprovalExpired", `Approval ${approvalId} has expired, so nothing was executed. Request a fresh approval by calling this tool with no approvalId.`, {
7440
+ action: input.action,
7441
+ approvalId
7442
+ });
7443
+ if (used) return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_APPROVAL_REJECTED", "ApprovalConsumed", `Approval ${approvalId} was already used once and approvals are single-use, so nothing was executed. Request a fresh approval by calling this tool with no approvalId.`, {
7444
+ action: input.action,
7445
+ approvalId
7446
+ });
7447
+ if ("approved" !== status) return approval_gate_block(input.command, APPROVAL_PENDING_STATUS, "E_APPROVAL_PENDING", "ApprovalPending", `Approval ${approvalId} has not been granted yet: ${input.description} Open the approval URL and approve it, then call this tool again with the same approvalId. Nothing was executed.`, {
7448
+ action: input.action,
7449
+ approvalId,
7450
+ status: status || "pending"
7451
+ });
7452
+ return {
7453
+ blocked: false,
7454
+ approvalId
7455
+ };
7456
+ }
7457
+ async function evaluateApproval(input) {
7458
+ const enabled = input.enabled ?? approvalGateEnabled();
7459
+ if (!enabled) return {
7460
+ blocked: false
7461
+ };
7462
+ const apiCheck = safeApiBase(resolveApiBase(input.api), input.api);
7463
+ if (!apiCheck.ok) return approval_gate_block(input.command, APPROVAL_REJECTED_STATUS, "E_CONFIG", "ApprovalConfigError", apiCheck.message, {
7464
+ action: input.action
7465
+ });
7466
+ const doFetch = input.fetchImpl ?? fetch;
7467
+ const fingerprint = actionFingerprint(input.action, input.scope);
7468
+ if (!String(input.approvalId || "").trim()) return requestApproval({
7469
+ base: apiCheck.base,
7470
+ fingerprint,
7471
+ input,
7472
+ doFetch
7473
+ });
7474
+ return verifyApproval({
7475
+ base: apiCheck.base,
7476
+ fingerprint,
7477
+ input,
7478
+ doFetch
7479
+ });
7480
+ }
7167
7481
  const LOGIN_HINT = "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
7168
7482
  const shares_schema = {
7169
7483
  name: "extension_shares",
@@ -7188,6 +7502,10 @@ const shares_schema = {
7188
7502
  type: "string",
7189
7503
  description: "Any URL of the share to revoke (previewUrl, zipUrl, viewUrl, or revokeUrl). The artifact id is read out of it, so the link you sent someone is enough to pull it back."
7190
7504
  },
7505
+ approvalId: {
7506
+ type: "string",
7507
+ description: "The approval handle returned by a prior approval-required response for a revoke. Revoking permanently burns a share and cannot be undone, so when the platform's approval gate is on this needs a human approval: call revoke once without this to get an approval id and URL, have a human approve at extension.dev, then call revoke again with the same id. Listing never needs it."
7508
+ },
7191
7509
  projectPath: {
7192
7510
  type: "string",
7193
7511
  description: "Path to the extension project root. Reconciles the platform's answer against this project's .extension.dev/shared-previews.json record. Read-only."
@@ -7435,8 +7753,26 @@ async function revokeShare(args) {
7435
7753
  message: supplied ? `The reference is malformed and nothing was sent to the platform: no share id could be read out of ${JSON.stringify(supplied.length > 200 ? `${supplied.slice(0, 200)}...` : supplied)}. A share id is gen_ followed by exactly 64 lowercase hex characters (32 on shares minted before 2026-07-30), and a url revokes only when it contains one whole. Check the ref for a copy-paste cut, or run action:"list" and copy the exact artifactId.` : 'Nothing to revoke. Pass artifactId (a gen_... id) or url (the previewUrl, zipUrl, viewUrl, or revokeUrl of the share). Run action:"list" to see both.'
7436
7754
  }
7437
7755
  });
7756
+ const token = resolveToken();
7757
+ if (token) {
7758
+ const gate = await evaluateApproval({
7759
+ command: "extension_shares",
7760
+ action: "extension_shares.revoke",
7761
+ scope: {
7762
+ artifactId: ref
7763
+ },
7764
+ description: `Permanently revoke share ${ref}, killing the link for everyone with no way to restore it.`,
7765
+ approvalId: args.approvalId,
7766
+ token,
7767
+ api: args.api
7768
+ });
7769
+ if (gate.blocked) return gate.envelope;
7770
+ }
7438
7771
  const result = await revokeArtifact({
7439
7772
  artifactId: ref,
7773
+ ...args.approvalId ? {
7774
+ approvalId: args.approvalId
7775
+ } : {},
7440
7776
  ...args.api ? {
7441
7777
  api: args.api
7442
7778
  } : {}
@@ -7523,6 +7859,9 @@ async function shares_handler(args) {
7523
7859
  ...args.projectPath ? {
7524
7860
  projectPath: args.projectPath
7525
7861
  } : {},
7862
+ ...args.approvalId ? {
7863
+ approvalId: args.approvalId
7864
+ } : {},
7526
7865
  ...args.api ? {
7527
7866
  api: args.api
7528
7867
  } : {}
@@ -12247,6 +12586,10 @@ const release_promote_schema = {
12247
12586
  type: "string",
12248
12587
  description: "Release notes markdown (optional)"
12249
12588
  },
12589
+ approvalId: {
12590
+ type: "string",
12591
+ description: "The approval handle returned by a prior approval-required response. Promoting changes what a public channel serves and is not reversible in place, so when the platform's approval gate is on this needs a human approval: call once without this to get an approval id and URL, have a human approve at extension.dev, then call again with the same id."
12592
+ },
12250
12593
  api: API_BASE
12251
12594
  },
12252
12595
  required: [
@@ -12276,10 +12619,34 @@ async function release_promote_handler(args) {
12276
12619
  const apiCheck = safeApiBase(resolveApiBase(args.api), args.api);
12277
12620
  if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message, "bad-config", "E_CONFIG");
12278
12621
  const url = `${apiCheck.base}/api/cli/release/promote`;
12622
+ const sourceChannel = args.sourceChannel ? String(args.sourceChannel).trim() : "";
12623
+ const gateBrowsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim()).filter(Boolean);
12624
+ const gate = await evaluateApproval({
12625
+ command: "extension_release_promote",
12626
+ action: "extension_release_promote",
12627
+ scope: {
12628
+ buildId: buildId.toLowerCase(),
12629
+ channel,
12630
+ ...sourceChannel ? {
12631
+ sourceChannel
12632
+ } : {},
12633
+ ...gateBrowsers.length ? {
12634
+ browsers: [
12635
+ ...gateBrowsers
12636
+ ].sort()
12637
+ } : {}
12638
+ },
12639
+ description: `Promote build ${buildId} to the ${channel} channel, changing what ${channel} serves.`,
12640
+ approvalId: args.approvalId,
12641
+ token,
12642
+ api: args.api
12643
+ });
12644
+ if (gate.blocked) return gate.envelope;
12279
12645
  const body = {
12280
12646
  buildId,
12281
12647
  channel
12282
12648
  };
12649
+ if (gate.approvalId) body.approvalId = gate.approvalId;
12283
12650
  if (args.sourceChannel) body.sourceChannel = String(args.sourceChannel).trim();
12284
12651
  if (Array.isArray(args.browsers) && args.browsers.length) body.browsers = args.browsers.map((b)=>String(b).trim()).filter(Boolean);
12285
12652
  if (args.version) body.version = String(args.version).trim();
@@ -12895,6 +13262,10 @@ const submit_schema = {
12895
13262
  type: "string",
12896
13263
  description: "Path to the extension project root, read only for the local STORE.md advisory check. Nothing local is uploaded; without it the check falls back to the server's working directory."
12897
13264
  },
13265
+ approvalId: {
13266
+ type: "string",
13267
+ description: "The approval handle returned by a prior approval-required response for a real submission. A real submission (dryRun:false) is irreversible and needs a human approval when the platform's approval gate is on: call once without this to get an approval id and URL, have a human approve at extension.dev, then call again with the same id. A dry run never needs it."
13268
+ },
12898
13269
  api: API_BASE
12899
13270
  },
12900
13271
  required: [
@@ -12926,6 +13297,7 @@ async function submit_handler(args) {
12926
13297
  if (!apiCheck.ok) return submit_fail("SubmitConfigError", apiCheck.message, "bad-config", "E_CONFIG");
12927
13298
  const url = `${apiCheck.base}/api/cli/stores/submit`;
12928
13299
  const dryRun = false !== args.dryRun;
13300
+ const channel = args.channel ? String(args.channel).trim() : "stable";
12929
13301
  const body = {
12930
13302
  browsers,
12931
13303
  buildSha,
@@ -12933,6 +13305,25 @@ async function submit_handler(args) {
12933
13305
  };
12934
13306
  if (args.channel) body.channel = String(args.channel).trim();
12935
13307
  if (args.version) body.version = String(args.version).trim();
13308
+ if (!dryRun) {
13309
+ const gate = await evaluateApproval({
13310
+ command: "extension_submit",
13311
+ action: "extension_submit",
13312
+ scope: {
13313
+ browsers: [
13314
+ ...browsers
13315
+ ].sort(),
13316
+ buildSha: buildSha.toLowerCase(),
13317
+ channel
13318
+ },
13319
+ description: `Submit build ${buildSha} to ${browsers.join(", ")} for store review.`,
13320
+ approvalId: args.approvalId,
13321
+ token,
13322
+ api: args.api
13323
+ });
13324
+ if (gate.blocked) return gate.envelope;
13325
+ if (gate.approvalId) body.approvalId = gate.approvalId;
13326
+ }
12936
13327
  let res;
12937
13328
  try {
12938
13329
  res = await fetch(url, {
@@ -0,0 +1,27 @@
1
+ export declare const APPROVAL_GATE_ENV = "EXTENSION_DEV_APPROVAL_GATE";
2
+ export declare const APPROVAL_REQUIRED_STATUS = "approval-required";
3
+ export declare const APPROVAL_PENDING_STATUS = "approval-pending";
4
+ export declare const APPROVAL_REJECTED_STATUS = "approval-rejected";
5
+ export declare function approvalGateEnabled(): boolean;
6
+ export type ApprovalScope = Record<string, string | string[]>;
7
+ export declare function actionFingerprint(action: string, scope: ApprovalScope): string;
8
+ export interface ApprovalGateInput {
9
+ command: string;
10
+ action: string;
11
+ scope: ApprovalScope;
12
+ description: string;
13
+ approvalId?: string;
14
+ token: string;
15
+ api?: string;
16
+ fetchImpl?: typeof fetch;
17
+ enabled?: boolean;
18
+ now?: number;
19
+ }
20
+ export type ApprovalGateResult = {
21
+ blocked: false;
22
+ approvalId?: string;
23
+ } | {
24
+ blocked: true;
25
+ envelope: string;
26
+ };
27
+ export declare function evaluateApproval(input: ApprovalGateInput): Promise<ApprovalGateResult>;
@@ -72,6 +72,7 @@ export declare function revokeArtifact(options: {
72
72
  artifactId: string;
73
73
  api?: string;
74
74
  token?: string;
75
+ approvalId?: string;
75
76
  fetchImpl?: FetchImpl;
76
77
  }): Promise<ArtifactsOutcome<ArtifactRevocation>>;
77
78
  export {};
@@ -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_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_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_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_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";
4
4
  export declare const ERROR_CODES: ErrorCode[];
5
5
  export interface EnvelopeError {
6
6
  code: ErrorCode | string;
@@ -0,0 +1,24 @@
1
+ export declare const DRAFT_SEEDED_EVENT = "draft_seeded";
2
+ export declare const FUNNEL_ENTRY = "mcp";
3
+ export declare const FUNNEL_SOURCE = "@extension.dev/mcp";
4
+ export declare const FUNNEL_EMITTED_FROM = "node";
5
+ export type SeedSource = "template" | "fork" | "blank-init";
6
+ export type CreationFunnelProperties = Record<string, string | number | boolean | null>;
7
+ export type CreationFunnelPayload = {
8
+ api_key: string;
9
+ event: string;
10
+ distinct_id: string;
11
+ timestamp: string;
12
+ properties: Record<string, string | number | boolean | null>;
13
+ };
14
+ export declare function posthogHost(): string;
15
+ export declare function funnelEnvironment(): string;
16
+ export declare function posthogKeyForEnvironment(environment: unknown): string;
17
+ export declare function shouldSendCreationFunnelEvent(): boolean;
18
+ export declare function seedRef(slug: unknown, commit: unknown): string;
19
+ export declare function creationFunnelPayload(event: string, properties: CreationFunnelProperties, now?: Date): CreationFunnelPayload | null;
20
+ export declare function captureCreationFunnelEvent(event: string, properties: CreationFunnelProperties, fetchImpl?: typeof fetch): Promise<CreationFunnelPayload | null>;
21
+ export declare function captureTemplateSeed(input: {
22
+ slug: string;
23
+ source?: SeedSource;
24
+ }, fetchImpl?: typeof fetch): Promise<CreationFunnelPayload | null>;
@@ -7,6 +7,7 @@ export interface ShareCorsVerdict {
7
7
  finalStatus: number;
8
8
  redirects: number;
9
9
  allowOrigin: string | null;
10
+ held: boolean;
10
11
  reason: string;
11
12
  }
12
13
  export declare function probeShareCors(options: {
@@ -1,6 +1,7 @@
1
- export declare const PINNED_COMMIT = "f7f4e6efb56a7e5ae08d58dbff3972d94af7d021";
1
+ export declare const PINNED_COMMIT = "1d04a3d62f4d82ab3f13a3388800d517d1627707";
2
2
  export type TemplateCatalogUtmSource = "mcp" | "mcp-create";
3
3
  export declare function templateCatalogUrl(slug: string, utmSource?: TemplateCatalogUtmSource): string;
4
+ export declare function resolvedTemplateCommit(): Promise<string>;
4
5
  export declare function templateMetaUrls(): Promise<string[]>;
5
6
  export declare function stripTemplatePathPrefix(slug: string, relativePath: string): string;
6
7
  export declare function templateFileUrls(slug: string, relativePath: string): Promise<string[]>;
@@ -31,6 +31,10 @@ export declare const schema: {
31
31
  type: string;
32
32
  description: string;
33
33
  };
34
+ approvalId: {
35
+ type: string;
36
+ description: string;
37
+ };
34
38
  api: {
35
39
  readonly type: "string";
36
40
  readonly description: "Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev)";
@@ -46,5 +50,6 @@ export declare function handler(args: {
46
50
  browsers?: string[];
47
51
  version?: string;
48
52
  releaseNotes?: string;
53
+ approvalId?: string;
49
54
  api?: string;
50
55
  }): Promise<string>;
@@ -18,6 +18,10 @@ export declare const schema: {
18
18
  type: string;
19
19
  description: string;
20
20
  };
21
+ approvalId: {
22
+ type: string;
23
+ description: string;
24
+ };
21
25
  projectPath: {
22
26
  type: string;
23
27
  description: string;
@@ -45,6 +49,7 @@ export declare function handler(args: {
45
49
  artifactId?: string;
46
50
  url?: string;
47
51
  projectPath?: string;
52
+ approvalId?: string;
48
53
  status?: string;
49
54
  limit?: number;
50
55
  api?: string;
@@ -6,6 +6,7 @@ export interface SubmitToolArgs {
6
6
  version?: string;
7
7
  dryRun?: boolean;
8
8
  projectPath?: string;
9
+ approvalId?: string;
9
10
  api?: string;
10
11
  }
11
12
  export declare const schema: {
@@ -43,6 +44,10 @@ export declare const schema: {
43
44
  type: string;
44
45
  description: string;
45
46
  };
47
+ approvalId: {
48
+ type: string;
49
+ description: string;
50
+ };
46
51
  api: {
47
52
  readonly type: "string";
48
53
  readonly description: "Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev)";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "10.7.0",
4
+ "version": "10.8.0",
5
5
  "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 30 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
6
6
  "mcpName": "io.github.extensiondev/mcp",
7
7
  "license": "Apache-2.0",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "10.7.0",
10
+ "version": "10.8.0",
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.7.0",
16
+ "version": "10.8.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"