@extension.dev/mcp 6.4.0 → 6.5.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 60+ 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": "6.4.0",
13
+ "version": "6.5.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 60+ 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": "6.4.0",
4
+ "version": "6.5.0",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "hello@extension.dev",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.5.0
4
+
5
+ A link you shared is no longer only as findable as the response that created
6
+ it. The shares you have made are now listable, and revocable, from the tool
7
+ that made them.
8
+
9
+ ### Added
10
+
11
+ - **`extension_shares` lists and revokes the links you have shared.**
12
+ `share:true` hands back a public link, an `expiresAt` and a `revokeUrl`, and
13
+ until now the only copy of that `revokeUrl` was the tool response and the
14
+ project's own `.extension.dev/shared-previews.json`. Neither could say what
15
+ was actually still live, and neither existed at all for a link shared from
16
+ another machine. `action:"list"` (the default) asks the platform for every
17
+ artifact the logged-in project owns and returns each one's `artifactId`,
18
+ name, version, live or dead state, `createdAt`, `expiresAt`, `revokedAt`,
19
+ size, and its `previewUrl`, `zipUrl` and `revokeUrl`. `previewUrl` and
20
+ `zipUrl` come back null for anything no longer live, because a revoked or
21
+ expired address resolves for nobody and echoing it back would invite passing
22
+ on a dead link; `revokeUrl` stays on every row.
23
+ - **`action:"revoke"` kills a link from where it was made.** It takes an
24
+ `artifactId` or, just as well, any URL of the share (`previewUrl`, `zipUrl`,
25
+ `viewUrl`, or `revokeUrl`), so the link you sent someone is enough to pull it
26
+ back without going to find a `curl` command. Revocation is permanent: the zip
27
+ is deleted and the id is burned, so a revoked link can never resolve again
28
+ and sharing the same build later mints a different one. The response says so
29
+ rather than reading like an undoable delete.
30
+ - **The platform's answer is reconciled with the project's record.** Pass
31
+ `projectPath` and every listed share is matched against
32
+ `.extension.dev/shared-previews.json`: a share the project recorded carries
33
+ its local `sharedAt`, `browser` and `distDir`, a share the platform knows
34
+ about but the project does not is flagged `remoteOnly` (shared from another
35
+ machine or another checkout), and a local record with no artifact behind it
36
+ is reported under `localOnly`. The tool only ever reads that file, never
37
+ rewrites it, so its append-only history stays intact.
38
+ - **A cut list is never passed off as the whole set.** When the platform
39
+ answers `truncated:true`, the response carries the count that came back
40
+ against the count that matched, says to raise `limit` or narrow with
41
+ `status:"live"`, and refuses to call a `localOnly` record dead, because a
42
+ share missing from a truncated list has not been shown to be gone.
43
+
44
+ ### Changed
45
+
46
+ - **A successful share now says where to find the link again.** The `share`
47
+ note and the `extension_preview_web` tool description both point at
48
+ `extension_shares`, at the moment the link exists and the question of how to
49
+ reach it later is about to come up.
50
+
3
51
  ## 6.4.0
4
52
 
5
53
  A build on your machine can now become a link someone else can open, and the
package/README.md CHANGED
@@ -133,6 +133,7 @@ cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/c
133
133
  | platform | `extension_whoami` | Show the stored login (never the token) |
134
134
  | platform | `extension_logout` | Remove stored credentials |
135
135
  | platform | `extension_preview_web` | Render a build in the web emulator, and share it as a link |
136
+ | platform | `extension_shares` | List every link you have shared, and revoke one permanently |
136
137
  | platform | `extension_publish` | Publish a shareable preview to extension.dev |
137
138
  | platform | `extension_release_promote` | Promote a build to a release channel, headless |
138
139
  | platform | `extension_deploy` | Submit to the Chrome, Firefox, and Edge stores through extension.dev |
@@ -143,6 +144,8 @@ Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension`
143
144
 
144
145
  An unpacked extension is unusually hard to hand to someone: the only way to look at a colleague's work-in-progress has been to take their zip and run untrusted code with real browser permissions on your own machine. `extension_preview_web` with `share: true` uploads the `dist/` it just built and returns a link that renders those exact bytes in the emulator. Whoever opens it installs nothing and signs in to nothing, which is what lets a designer, a PM, or a reviewer into the loop at all. Sharing needs auth (`extension_login` or `EXTENSION_DEV_TOKEN`), the link expires, and `DELETE`ing the returned `revokeUrl` with the same token kills it early. Revocation is permanent and re-sharing mints a new link, so that `revokeUrl` is the only handle to the link you just made; every share is also appended to `.extension.dev/shared-previews.json` in the project (gitignored) so it survives losing the tool output. Without `share`, the tool returns a local-only deep link and uploads nothing.
145
146
 
147
+ `extension_shares` is the other half of that: it lists every link the token has shared, live and dead, with the `previewUrl` and `revokeUrl` of each, and revokes one by `artifactId` or by pasting any of its URLs. Pass `projectPath` and it reconciles the platform's answer with the project's own record, so a link shared from another machine shows up as `remoteOnly` and a record with nothing behind it any more shows up under `localOnly`. It never rewrites the local file.
148
+
146
149
  That is a different job from shipping. Use `share` for the build you are holding right now; use `extension_publish` and `extension_release_promote` below for builds your CI has released.
147
150
 
148
151
  ## From preview to store
package/dist/module.js CHANGED
@@ -566,6 +566,12 @@ __webpack_require__.d(reload_namespaceObject, {
566
566
  handler: ()=>reload_handler,
567
567
  schema: ()=>reload_schema
568
568
  });
569
+ var shares_namespaceObject = {};
570
+ __webpack_require__.r(shares_namespaceObject);
571
+ __webpack_require__.d(shares_namespaceObject, {
572
+ handler: ()=>shares_handler,
573
+ schema: ()=>shares_schema
574
+ });
569
575
  var source_inspect_namespaceObject = {};
570
576
  __webpack_require__.r(source_inspect_namespaceObject);
571
577
  __webpack_require__.d(source_inspect_namespaceObject, {
@@ -623,7 +629,7 @@ __webpack_require__.d(whoami_namespaceObject, {
623
629
  handler: ()=>whoami_handler,
624
630
  schema: ()=>whoami_schema
625
631
  });
626
- var package_namespaceObject = JSON.parse('{"rE":"6.4.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
632
+ var package_namespaceObject = JSON.parse('{"rE":"6.5.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
627
633
  function credentialsPath() {
628
634
  if ("win32" === process.platform) {
629
635
  const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
@@ -4499,6 +4505,22 @@ function loadExisting(file) {
4499
4505
  unreadable: true
4500
4506
  };
4501
4507
  }
4508
+ function readSharedPreviews(projectPath) {
4509
+ const file = sharedPreviewsPath(projectPath);
4510
+ if (!node_fs.existsSync(file)) return {
4511
+ path: file,
4512
+ exists: false,
4513
+ unreadable: false,
4514
+ entries: []
4515
+ };
4516
+ const existing = loadExisting(file);
4517
+ return {
4518
+ path: file,
4519
+ exists: true,
4520
+ unreadable: existing.unreadable,
4521
+ entries: existing.entries.filter((entry)=>entry && "string" == typeof entry.artifactId)
4522
+ };
4523
+ }
4502
4524
  function recordSharedPreview(projectPath, entry) {
4503
4525
  const file = sharedPreviewsPath(projectPath);
4504
4526
  let preserved;
@@ -4629,7 +4651,7 @@ async function buildShare(projectPath, distDir, manifest, browser) {
4629
4651
  serves: "uploaded-local-build",
4630
4652
  localBuildUploaded: true,
4631
4653
  record,
4632
- note: "Anyone with this link can open the build you just made, running in the emulator. No install, no sign-in, no dev server. They can also download the whole build as a zip from zipUrl, so the link hands over the built code. It stays live until expiresAt; DELETE revokeUrl with the same token to kill it sooner, and a revoked link stays dead. revokeUrl is the only handle that pulls this link early and re-sharing mints a different one, so " + (record.recorded ? `it was also written to ${record.path} (record.path), which lists every share from this project.` : `keep it: ${record.note}`) + (record.warning ? ` ${record.warning}` : "")
4654
+ note: "Anyone with this link can open the build you just made, running in the emulator. No install, no sign-in, no dev server. They can also download the whole build as a zip from zipUrl, so the link hands over the built code. It stays live until expiresAt; DELETE revokeUrl with the same token to kill it sooner, and a revoked link stays dead. revokeUrl is the only handle that pulls this link early and re-sharing mints a different one, so " + (record.recorded ? `it was also written to ${record.path} (record.path), which lists every share from this project.` : `keep it: ${record.note}`) + " To find this link again later, or to pull it back once it has left this conversation, run extension_shares: it lists every link this token has shared with its live or dead state, and revokes one by artifactId or by pasting any of its URLs." + (record.warning ? ` ${record.warning}` : "")
4633
4655
  };
4634
4656
  }
4635
4657
  function detectSurfaces(manifest) {
@@ -4652,7 +4674,7 @@ function detectSurfaces(manifest) {
4652
4674
  }
4653
4675
  const preview_web_schema = {
4654
4676
  name: "extension_preview_web",
4655
- description: "Preview an in-progress extension in the web emulator (no real browser). Builds the project (unless build:false), points preview.extension.dev at the dist directory over the dev-only preview://build scheme, and returns a deep link plus a loadability check against its dev server. preview is the author's front door: it renders YOUR build and carries the Emulated/Real lane toggle and the Trace tab. Pass surface:\"inspect\" to render in inspect.extension.dev instead (the evaluator's door, for fixture and forensic work). Pass share:true to UPLOAD the dist you just built and get back a public link (share.previewUrl) that renders those exact bytes for anyone who opens it, with no install and no dev server, and that also serves the whole build as a downloadable zip. Sharing needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days), so a local folder with no project on the platform cannot share. Use share:true whenever the build has to reach someone who is not at this machine, since the plain deepLink only resolves locally.",
4677
+ description: "Preview an in-progress extension in the web emulator (no real browser). Builds the project (unless build:false), points preview.extension.dev at the dist directory over the dev-only preview://build scheme, and returns a deep link plus a loadability check against its dev server. preview is the author's front door: it renders YOUR build and carries the Emulated/Real lane toggle and the Trace tab. Pass surface:\"inspect\" to render in inspect.extension.dev instead (the evaluator's door, for fixture and forensic work). Pass share:true to UPLOAD the dist you just built and get back a public link (share.previewUrl) that renders those exact bytes for anyone who opens it, with no install and no dev server, and that also serves the whole build as a downloadable zip. Sharing needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days), so a local folder with no project on the platform cannot share. Use share:true whenever the build has to reach someone who is not at this machine, since the plain deepLink only resolves locally. Shared links do not disappear when this response scrolls away: extension_shares lists every link this token has shared and revokes any of them.",
4656
4678
  inputSchema: {
4657
4679
  type: "object",
4658
4680
  properties: {
@@ -4734,7 +4756,7 @@ const preview_web_schema = {
4734
4756
  share: {
4735
4757
  type: "boolean",
4736
4758
  default: false,
4737
- description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes in the emulator for anyone who opens it: no install, no sign-in, no dev server. The link also serves the whole build as a downloadable zip (share.zipUrl), so sharing it hands over the built code. Needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days); without one this returns a login hint and never fails the local preview. The link stays live until share.expiresAt, and DELETEing share.revokeUrl with the same token kills it sooner; a revoked link stays dead, and re-sharing the same build returns a new link. Because re-sharing never reproduces the old link, every successful share is also appended to .extension.dev/shared-previews.json in the project (gitignored) so the revoke handle survives losing this response."
4759
+ description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes in the emulator for anyone who opens it: no install, no sign-in, no dev server. The link also serves the whole build as a downloadable zip (share.zipUrl), so sharing it hands over the built code. Needs a token scoped to an existing extension.dev workspace and project (extension_login or EXTENSION_DEV_TOKEN, valid up to 7 days); without one this returns a login hint and never fails the local preview. The link stays live until share.expiresAt, and DELETEing share.revokeUrl with the same token kills it sooner; a revoked link stays dead, and re-sharing the same build returns a new link. Because re-sharing never reproduces the old link, every successful share is also appended to .extension.dev/shared-previews.json in the project (gitignored) so the revoke handle survives losing this response, and extension_shares lists what is actually live on the platform and revokes any of it by id or by URL."
4738
4760
  }
4739
4761
  },
4740
4762
  required: [
@@ -4869,6 +4891,402 @@ async function preview_web_handler(args) {
4869
4891
  });
4870
4892
  }
4871
4893
  }
4894
+ const ARTIFACT_ID = /^gen_[0-9a-f]{32}$/;
4895
+ const ARTIFACT_ID_ANYWHERE = /gen_[0-9a-f]{32}/;
4896
+ function parseArtifactRef(input) {
4897
+ const raw = String(input ?? "").trim();
4898
+ if (!raw) return null;
4899
+ if (ARTIFACT_ID.test(raw)) return raw;
4900
+ let parsed = null;
4901
+ try {
4902
+ parsed = new URL(raw);
4903
+ } catch {
4904
+ parsed = null;
4905
+ }
4906
+ if (parsed) {
4907
+ const fromQuery = parsed.searchParams.get("preview");
4908
+ if (fromQuery && ARTIFACT_ID.test(fromQuery.trim())) return fromQuery.trim();
4909
+ const segments = parsed.pathname.split("/").filter(Boolean);
4910
+ for(let i = segments.length - 1; i >= 0; i -= 1){
4911
+ const segment = decodeURIComponent(segments[i]);
4912
+ if (ARTIFACT_ID.test(segment)) return segment;
4913
+ }
4914
+ }
4915
+ const loose = ARTIFACT_ID_ANYWHERE.exec(raw);
4916
+ return loose ? loose[0] : null;
4917
+ }
4918
+ function authError(name) {
4919
+ return {
4920
+ ok: false,
4921
+ error: {
4922
+ name,
4923
+ message: "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
4924
+ }
4925
+ };
4926
+ }
4927
+ async function readBody(res) {
4928
+ const text = await res.text();
4929
+ try {
4930
+ return JSON.parse(text);
4931
+ } catch {
4932
+ return {
4933
+ message: text
4934
+ };
4935
+ }
4936
+ }
4937
+ async function listArtifacts(options = {}) {
4938
+ const token = options.token ?? resolveToken();
4939
+ if (!token) return authError("SharesAuthError");
4940
+ const apiCheck = safeApiBase(resolveApiBase(options.api));
4941
+ if (!apiCheck.ok) return {
4942
+ ok: false,
4943
+ error: {
4944
+ name: "SharesConfigError",
4945
+ message: apiCheck.message
4946
+ }
4947
+ };
4948
+ const url = new URL(`${apiCheck.base}/api/artifacts`);
4949
+ if (null != options.limit) url.searchParams.set("limit", String(options.limit));
4950
+ if (options.liveOnly) url.searchParams.set("status", "live");
4951
+ const doFetch = options.fetchImpl ?? fetch;
4952
+ let res;
4953
+ try {
4954
+ res = await doFetch(url.toString(), {
4955
+ headers: {
4956
+ authorization: `Bearer ${token}`,
4957
+ accept: "application/json"
4958
+ }
4959
+ });
4960
+ } catch (err) {
4961
+ return {
4962
+ ok: false,
4963
+ error: {
4964
+ name: "SharesNetworkError",
4965
+ message: `Could not reach ${url.toString()}: ${err?.message || err}`
4966
+ }
4967
+ };
4968
+ }
4969
+ const data = await readBody(res);
4970
+ if (401 === res.status) return authError("SharesAuthError");
4971
+ if (!res.ok) return {
4972
+ ok: false,
4973
+ error: {
4974
+ name: "SharesListError",
4975
+ status: res.status,
4976
+ message: `Listing shares failed (${res.status}): ${data?.message || "unknown error"}`
4977
+ }
4978
+ };
4979
+ const artifacts = Array.isArray(data.artifacts) ? data.artifacts : [];
4980
+ const num = (value, fallback)=>"number" == typeof value && Number.isFinite(value) ? value : fallback;
4981
+ return {
4982
+ ok: true,
4983
+ data: {
4984
+ artifacts,
4985
+ count: num(data.count, artifacts.length),
4986
+ matched: num(data.matched, artifacts.length),
4987
+ limit: num(data.limit, artifacts.length),
4988
+ truncated: true === data.truncated,
4989
+ scanned: num(data.scanned, 0)
4990
+ }
4991
+ };
4992
+ }
4993
+ async function revokeArtifact(options) {
4994
+ const token = options.token ?? resolveToken();
4995
+ if (!token) return authError("SharesAuthError");
4996
+ const apiCheck = safeApiBase(resolveApiBase(options.api));
4997
+ if (!apiCheck.ok) return {
4998
+ ok: false,
4999
+ error: {
5000
+ name: "SharesConfigError",
5001
+ message: apiCheck.message
5002
+ }
5003
+ };
5004
+ const url = `${apiCheck.base}/api/artifacts/${encodeURIComponent(options.artifactId)}`;
5005
+ const doFetch = options.fetchImpl ?? fetch;
5006
+ let res;
5007
+ try {
5008
+ res = await doFetch(url, {
5009
+ method: "DELETE",
5010
+ headers: {
5011
+ authorization: `Bearer ${token}`,
5012
+ accept: "application/json"
5013
+ }
5014
+ });
5015
+ } catch (err) {
5016
+ return {
5017
+ ok: false,
5018
+ error: {
5019
+ name: "SharesNetworkError",
5020
+ message: `Could not reach ${url}: ${err?.message || err}`
5021
+ }
5022
+ };
5023
+ }
5024
+ const data = await readBody(res);
5025
+ if (401 === res.status) return authError("SharesAuthError");
5026
+ if (404 === res.status) return {
5027
+ ok: false,
5028
+ error: {
5029
+ name: "SharesNotFoundError",
5030
+ status: 404,
5031
+ message: `The platform has no live share ${options.artifactId} for this token. It may already be revoked, already expired, or owned by a different project than the one this token is scoped to.`
5032
+ }
5033
+ };
5034
+ if (!res.ok) return {
5035
+ ok: false,
5036
+ error: {
5037
+ name: "SharesRevokeError",
5038
+ status: res.status,
5039
+ message: `Revoking ${options.artifactId} failed (${res.status}): ${data?.message || "unknown error"}`
5040
+ }
5041
+ };
5042
+ return {
5043
+ ok: true,
5044
+ data: {
5045
+ artifactId: options.artifactId,
5046
+ revoked: true === data.revoked,
5047
+ ..."string" == typeof data.revokedAt ? {
5048
+ revokedAt: data.revokedAt
5049
+ } : {}
5050
+ }
5051
+ };
5052
+ }
5053
+ const LOGIN_HINT = "Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
5054
+ const shares_schema = {
5055
+ name: "extension_shares",
5056
+ description: "List and revoke the public preview links this token has shared (the links extension_preview_web share:true hands out). action:\"list\" (default) asks the platform for every artifact owned by the logged-in project and returns each one's artifactId, name, version, live/dead state, createdAt, expiresAt, revokedAt, size, and its previewUrl, zipUrl and revokeUrl, so a link you lost the response for is findable again. action:\"revoke\" kills one by artifactId or by pasting any of its URLs; revocation is PERMANENT (the id is burned and re-sharing mints a different link), so it cannot be undone. Pass projectPath to reconcile the platform's answer with .extension.dev/shared-previews.json, the project's own append-only record: a share made on another machine shows up as remoteOnly, and a record with no live artifact behind it shows up under localOnly. Needs the same token as sharing (extension_login or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint instead of failing. Read-only for the local file: this tool never rewrites shared-previews.json.",
5057
+ inputSchema: {
5058
+ type: "object",
5059
+ properties: {
5060
+ action: {
5061
+ type: "string",
5062
+ enum: [
5063
+ "list",
5064
+ "revoke"
5065
+ ],
5066
+ default: "list",
5067
+ description: "list reads every share this token owns; revoke permanently kills one and cannot be undone."
5068
+ },
5069
+ artifactId: {
5070
+ type: "string",
5071
+ description: "Which share to revoke (the gen_... id from a share response or from action:\"list\"). Required for revoke unless url is given."
5072
+ },
5073
+ url: {
5074
+ type: "string",
5075
+ 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."
5076
+ },
5077
+ projectPath: {
5078
+ type: "string",
5079
+ description: "Path to the extension project root. Reconciles the platform's answer against this project's .extension.dev/shared-previews.json record. Read-only."
5080
+ },
5081
+ status: {
5082
+ type: "string",
5083
+ enum: [
5084
+ "all",
5085
+ "live"
5086
+ ],
5087
+ default: "all",
5088
+ description: "all (default) includes expired and revoked shares, which is what makes a dead link explainable; live returns only links that still resolve."
5089
+ },
5090
+ limit: {
5091
+ type: "number",
5092
+ description: "How many shares to return, 1 to 200 (platform default 100). A cut list comes back with truncated:true."
5093
+ },
5094
+ api: {
5095
+ type: "string",
5096
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)."
5097
+ }
5098
+ },
5099
+ required: []
5100
+ }
5101
+ };
5102
+ function localIndex(entries) {
5103
+ const index = new Map();
5104
+ for (const entry of entries){
5105
+ const existing = index.get(entry.artifactId);
5106
+ if (!existing || String(entry.sharedAt) > String(existing.sharedAt)) index.set(entry.artifactId, entry);
5107
+ }
5108
+ return index;
5109
+ }
5110
+ function localOnlyStatus(entry, truncated, now) {
5111
+ if (truncated) return "unknown: the platform list was cut short, so this share may simply be past the returned window.";
5112
+ const expiresAt = entry.expiresAt ? Date.parse(entry.expiresAt) : NaN;
5113
+ if (Number.isFinite(expiresAt) && expiresAt <= now) return "expired: its own expiresAt has passed, and the platform no longer lists it.";
5114
+ return "not owned by this token: expired and pruned, revoked long ago, or shared while logged in to a different project.";
5115
+ }
5116
+ async function listShares(args) {
5117
+ const local = args.projectPath ? readSharedPreviews(args.projectPath) : void 0;
5118
+ const localRecord = local ? {
5119
+ path: local.path,
5120
+ exists: local.exists,
5121
+ entries: local.entries.length,
5122
+ ...local.unreadable ? {
5123
+ unreadable: "The file exists but could not be parsed, so no local reconciliation was possible."
5124
+ } : {}
5125
+ } : {
5126
+ path: null,
5127
+ note: "Pass projectPath to reconcile this list against the project's own share record."
5128
+ };
5129
+ const listing = await listArtifacts({
5130
+ liveOnly: "live" === args.status,
5131
+ ...null != args.limit ? {
5132
+ limit: args.limit
5133
+ } : {},
5134
+ ...args.api ? {
5135
+ api: args.api
5136
+ } : {}
5137
+ });
5138
+ if (!listing.ok) {
5139
+ const isAuth = "SharesAuthError" === listing.error.name;
5140
+ return JSON.stringify({
5141
+ ok: true,
5142
+ action: "list",
5143
+ server: {
5144
+ listed: false,
5145
+ errorName: listing.error.name,
5146
+ reason: listing.error.message,
5147
+ ...isAuth ? {
5148
+ loginHint: LOGIN_HINT
5149
+ } : {}
5150
+ },
5151
+ shares: [],
5152
+ localOnly: (local?.entries ?? []).map((entry)=>({
5153
+ ...entry
5154
+ })),
5155
+ localRecord,
5156
+ note: isAuth ? `The platform was not asked, so live and dead cannot be told apart here. ${LOGIN_HINT} localOnly is this project's own record of every link it ever shared, revoke handles included.` : "The platform could not be reached, so localOnly below is this project's own record and not a statement about what is still live."
5157
+ });
5158
+ }
5159
+ const now = Date.now();
5160
+ const byId = localIndex(local?.entries ?? []);
5161
+ const seen = new Set();
5162
+ const shares = listing.data.artifacts.map((artifact)=>{
5163
+ seen.add(artifact.artifactId);
5164
+ const entry = byId.get(artifact.artifactId);
5165
+ return {
5166
+ ...artifact,
5167
+ recordedLocally: Boolean(entry),
5168
+ ...entry ? {
5169
+ sharedAt: entry.sharedAt,
5170
+ ...entry.browser ? {
5171
+ browser: entry.browser
5172
+ } : {},
5173
+ ...entry.distDir ? {
5174
+ distDir: entry.distDir
5175
+ } : {}
5176
+ } : local ? {
5177
+ remoteOnly: "Not in this project's record: shared from another machine or another checkout of the project."
5178
+ } : {}
5179
+ };
5180
+ });
5181
+ const localOnly = (local?.entries ?? []).filter((entry)=>!seen.has(entry.artifactId)).map((entry)=>({
5182
+ ...entry,
5183
+ status: localOnlyStatus(entry, listing.data.truncated, now)
5184
+ }));
5185
+ const liveCount = shares.filter((share)=>share.live).length;
5186
+ return JSON.stringify({
5187
+ ok: true,
5188
+ action: "list",
5189
+ server: {
5190
+ listed: true,
5191
+ count: listing.data.count,
5192
+ matched: listing.data.matched,
5193
+ limit: listing.data.limit,
5194
+ truncated: listing.data.truncated,
5195
+ scanned: listing.data.scanned,
5196
+ ...listing.data.truncated ? {
5197
+ truncatedNote: `This is not the whole set: ${listing.data.count} of ${listing.data.matched} matched shares came back at limit ${listing.data.limit}. Raise limit (max 200) or pass status:"live" to narrow it, and do not read a missing share as revoked.`
5198
+ } : {}
5199
+ },
5200
+ shares,
5201
+ ...local ? {
5202
+ localOnly
5203
+ } : {},
5204
+ localRecord,
5205
+ message: `${liveCount} of ${shares.length} listed shares still resolve${local ? `; ${localOnly.length} local record ${1 === localOnly.length ? "entry has" : "entries have"} no artifact behind them` : ""}. Revoke one with action:"revoke" and its artifactId or any of its URLs.`,
5206
+ note: "previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned and re-sharing the same build mints a different link."
5207
+ });
5208
+ }
5209
+ async function revokeShare(args) {
5210
+ const ref = parseArtifactRef(args.artifactId || args.url || "");
5211
+ if (!ref) return JSON.stringify({
5212
+ ok: false,
5213
+ action: "revoke",
5214
+ error: {
5215
+ name: "SharesInputError",
5216
+ message: "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."
5217
+ }
5218
+ });
5219
+ const result = await revokeArtifact({
5220
+ artifactId: ref,
5221
+ ...args.api ? {
5222
+ api: args.api
5223
+ } : {}
5224
+ });
5225
+ const local = args.projectPath ? readSharedPreviews(args.projectPath) : void 0;
5226
+ const entry = local ? localIndex(local.entries).get(ref) : void 0;
5227
+ const recordNote = local ? entry ? `${local.path} still lists this share as its own append-only history and was not rewritten, so the entry stays with its original sharedAt. The platform is the truth for whether a link resolves.` : `${local.path} has no entry for this share, so it was made from another machine or another checkout.` : void 0;
5228
+ if (!result.ok) {
5229
+ const isAuth = "SharesAuthError" === result.error.name;
5230
+ return JSON.stringify({
5231
+ ok: false,
5232
+ action: "revoke",
5233
+ artifactId: ref,
5234
+ error: {
5235
+ name: result.error.name,
5236
+ message: result.error.message
5237
+ },
5238
+ ...isAuth ? {
5239
+ loginHint: LOGIN_HINT
5240
+ } : {},
5241
+ ...recordNote ? {
5242
+ recordNote
5243
+ } : {}
5244
+ });
5245
+ }
5246
+ return JSON.stringify({
5247
+ ok: true,
5248
+ action: "revoke",
5249
+ artifactId: ref,
5250
+ revoked: result.data.revoked,
5251
+ ...result.data.revokedAt ? {
5252
+ revokedAt: result.data.revokedAt
5253
+ } : {},
5254
+ ...recordNote ? {
5255
+ recordNote
5256
+ } : {},
5257
+ note: "The link is dead for everyone, permanently: the zip is deleted and the id is burned, so it can never resolve again. Sharing the same build later returns a different link, and anyone holding the old one gets nothing."
5258
+ });
5259
+ }
5260
+ async function shares_handler(args) {
5261
+ if ("revoke" === args.action) return revokeShare({
5262
+ ...args.artifactId ? {
5263
+ artifactId: args.artifactId
5264
+ } : {},
5265
+ ...args.url ? {
5266
+ url: args.url
5267
+ } : {},
5268
+ ...args.projectPath ? {
5269
+ projectPath: args.projectPath
5270
+ } : {},
5271
+ ...args.api ? {
5272
+ api: args.api
5273
+ } : {}
5274
+ });
5275
+ return listShares({
5276
+ ...args.projectPath ? {
5277
+ projectPath: args.projectPath
5278
+ } : {},
5279
+ ...args.status ? {
5280
+ status: args.status
5281
+ } : {},
5282
+ ...null != args.limit ? {
5283
+ limit: args.limit
5284
+ } : {},
5285
+ ...args.api ? {
5286
+ api: args.api
5287
+ } : {}
5288
+ });
5289
+ }
4872
5290
  const get_template_source_schema = {
4873
5291
  name: "extension_get_template_source",
4874
5292
  description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
@@ -10652,6 +11070,7 @@ const tools = [
10652
11070
  start_namespaceObject,
10653
11071
  preview_namespaceObject,
10654
11072
  preview_web_namespaceObject,
11073
+ shares_namespaceObject,
10655
11074
  stop_namespaceObject,
10656
11075
  get_template_source_namespaceObject,
10657
11076
  manifest_validate_namespaceObject,
@@ -0,0 +1,56 @@
1
+ type FetchImpl = typeof fetch;
2
+ export interface ListedArtifact {
3
+ artifactId: string;
4
+ kind?: string;
5
+ name?: string;
6
+ slug?: string;
7
+ version?: string;
8
+ live: boolean;
9
+ createdAt?: string;
10
+ expiresAt?: string | null;
11
+ revokedAt?: string | null;
12
+ sizeBytes?: number | null;
13
+ previewUrl?: string | null;
14
+ viewUrl?: string | null;
15
+ zipUrl?: string | null;
16
+ revokeUrl?: string;
17
+ }
18
+ export interface ArtifactListing {
19
+ artifacts: ListedArtifact[];
20
+ count: number;
21
+ matched: number;
22
+ limit: number;
23
+ truncated: boolean;
24
+ scanned: number;
25
+ }
26
+ export interface ArtifactRevocation {
27
+ artifactId: string;
28
+ revoked: boolean;
29
+ revokedAt?: string;
30
+ }
31
+ export type ArtifactsOutcome<T> = {
32
+ ok: true;
33
+ data: T;
34
+ } | {
35
+ ok: false;
36
+ error: {
37
+ name: string;
38
+ message: string;
39
+ status?: number;
40
+ };
41
+ };
42
+ export declare function parseArtifactRef(input: string): string | null;
43
+ export declare function listArtifacts(options?: {
44
+ limit?: number;
45
+ liveOnly?: boolean;
46
+ api?: string;
47
+ token?: string;
48
+ fetchImpl?: FetchImpl;
49
+ }): Promise<ArtifactsOutcome<ArtifactListing>>;
50
+ export declare function revokeArtifact(options: {
51
+ artifactId: string;
52
+ api?: string;
53
+ token?: string;
54
+ fetchImpl?: FetchImpl;
55
+ }): Promise<ArtifactsOutcome<ArtifactRevocation>>;
56
+ export {};
@@ -23,4 +23,11 @@ export interface ShareRecordOutcome {
23
23
  warning?: string;
24
24
  }
25
25
  export declare function sharedPreviewsPath(projectPath: string): string;
26
+ export interface SharedPreviewsFile {
27
+ path: string;
28
+ exists: boolean;
29
+ unreadable: boolean;
30
+ entries: SharedPreviewEntry[];
31
+ }
32
+ export declare function readSharedPreviews(projectPath: string): SharedPreviewsFile;
26
33
  export declare function recordSharedPreview(projectPath: string, entry: SharedPreviewEntry): ShareRecordOutcome;
@@ -0,0 +1,51 @@
1
+ export declare const schema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ action: {
8
+ type: string;
9
+ enum: string[];
10
+ default: string;
11
+ description: string;
12
+ };
13
+ artifactId: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ url: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ projectPath: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ status: {
26
+ type: string;
27
+ enum: string[];
28
+ default: string;
29
+ description: string;
30
+ };
31
+ limit: {
32
+ type: string;
33
+ description: string;
34
+ };
35
+ api: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ };
40
+ required: never[];
41
+ };
42
+ };
43
+ export declare function handler(args: {
44
+ action?: string;
45
+ artifactId?: string;
46
+ url?: string;
47
+ projectPath?: string;
48
+ status?: string;
49
+ limit?: number;
50
+ api?: string;
51
+ }): Promise<string>;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "6.4.0",
5
- "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 35 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.",
4
+ "version": "6.5.0",
5
+ "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 36 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",
8
8
  "author": {
package/server.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
3
  "name": "io.github.extensiondev/mcp",
4
- "description": "Build, run, inspect, and publish browser extensions from any MCP client. 35 tools, 11 browsers.",
4
+ "description": "Build, run, inspect, and publish browser extensions from any MCP client. 36 tools, 11 browsers.",
5
5
  "repository": {
6
6
  "url": "https://github.com/extensiondev/mcp",
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://extension.dev",
10
- "version": "6.4.0",
10
+ "version": "6.5.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": "6.4.0",
16
+ "version": "6.5.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"