@extension.dev/mcp 5.5.0 → 5.5.1

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.
@@ -2,7 +2,7 @@
2
2
  "name": "extensiondev-mcp",
3
3
  "owner": {
4
4
  "name": "Cezar Augusto",
5
- "email": "boss@cezaraugusto.net",
5
+ "email": "hello@extension.dev",
6
6
  "url": "https://extension.dev"
7
7
  },
8
8
  "plugins": [
@@ -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": "5.5.0",
13
+ "version": "5.5.1",
14
14
  "category": "development",
15
15
  "author": {
16
16
  "name": "Cezar Augusto"
@@ -1,10 +1,10 @@
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": "5.5.0",
4
+ "version": "5.5.1",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
- "email": "boss@cezaraugusto.net",
7
+ "email": "hello@extension.dev",
8
8
  "url": "https://cezaraugusto.com"
9
9
  },
10
10
  "homepage": "https://github.com/extensiondev/mcp",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.5.1
4
+
5
+ The store journey stops being write-only after submit, and the token
6
+ surfaces start telling the truth about lifetimes and API bases.
7
+
8
+ ### Added
9
+
10
+ - `extension_store_status` (tool 33): the post-submit sibling of
11
+ `extension_deploy`. Reads the project's public registry
12
+ (`stores/health.json`, `stores/status.json`, `stores/submissions.json`)
13
+ and reports per store whether it is configured, its latest credential
14
+ health check, the last recorded submission (version, status, store
15
+ URL, submitted-at), and the latest review status. Normalizes both the
16
+ v3 merged status schema and legacy v2 poller documents. Defaults to
17
+ the logged-in project; accepts `workspace` + `project` overrides like
18
+ `extension_release_list`. A configured store with a failing credential
19
+ reports `configured: true` with `health.ok: false` (rotate the
20
+ credential, deep-linked), never "not configured".
21
+
22
+ ### Fixed
23
+
24
+ - `extension_whoami` no longer asserts a bare `api` field that could
25
+ misstate the platform base (a login minted via a localhost dev server
26
+ kept reporting that dead base for a token that authenticates against
27
+ production). The recorded login base is now labeled
28
+ `apiRecordedAtLogin`, `apiDefault` reports what authenticated tools
29
+ actually target, the message flags any divergence, and a set
30
+ `EXTENSION_DEV_TOKEN` is disclosed as outranking the stored login.
31
+ - The 7-day token TTL (server-enforced) is now stated everywhere a CI
32
+ author looks: `extension_login`'s description and success/pending
33
+ results, `extension_whoami`'s `tokenTtlNote` (with the deep console
34
+ Access tokens URL), and the auth prose of `extension_deploy` and
35
+ `extension_release_promote` (`extension_publish`'s auth envelope is
36
+ byte-frozen and unchanged).
37
+ - `extension_deploy`'s description says the per-store rows in the result
38
+ are the verdict to trust: the platform's bare preflight line does not
39
+ check store health. A real (non-dry-run) submission now points at
40
+ `extension_store_status` for tracking.
41
+
3
42
  ## 5.5.0
4
43
 
5
44
  The debug surfaces learn to point at things: tabs are targetable by
@@ -644,7 +683,7 @@ this release graduates that line to stable and becomes `latest`.
644
683
 
645
684
  # @extension.dev/mcp, Changelog
646
685
 
647
- ## Unreleased, agent-bridge tools
686
+ ## 5.5.1, agent-bridge tools
648
687
 
649
688
  Adds the MCP client surface for the Extension.js **agent bridge** (dev-time
650
689
  observe + act + inspect). All new tools shell out to the `extension` CLI verbs
@@ -679,7 +718,7 @@ Internal: `lib/act` (CLI shell-out helper), `lib/exec.runExtensionCli` (capture)
679
718
  `lib/cdp.getClosedShadowRoots`. Test infra aligned to the workspace vitest
680
719
  catalog.
681
720
 
682
- ## Unreleased, login (auth tools)
721
+ ## 5.5.1, login (auth tools)
683
722
 
684
723
  Adds the missing `login` flow so `extension_publish` no longer requires the user
685
724
  to mint and export `EXTENSION_DEV_TOKEN` by hand. Auth stays auth-AWARE: the
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  # @extension.dev/mcp [![Version][npm-version-image]][npm-version-url] [![Downloads][npm-downloads-image]][npm-downloads-url] [![Discord][discord-image]][discord-url]
9
9
 
10
- > Give your AI agent hands for browser extension development. 30 MCP tools that scaffold, run, inspect, debug, and publish cross-browser extensions.
10
+ > Give your AI agent hands for browser extension development. 33 MCP tools that scaffold, run, inspect, debug, and publish cross-browser extensions.
11
11
 
12
12
  <img alt="Logo" align="right" src="https://media.extension.land/brand/extension-dev/logo-dock.png" width="20.7%" />
13
13
 
@@ -127,7 +127,7 @@ Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension`
127
127
 
128
128
  ## From preview to store
129
129
 
130
- The platform tools connect agents to [extension.dev](https://extension.dev): `extension_login` runs a GitHub device-code flow and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build into a shareable preview URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. `extension_deploy` submits a built extension to the Chrome Web Store, Edge Add-ons, and Firefox AMO through extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI, it defaults to a dry run and store credentials are never tool arguments.
130
+ The platform tools connect agents to [extension.dev](https://extension.dev): `extension_login` runs a GitHub device-code flow and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build into a shareable preview URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. `extension_deploy` submits a built extension to the Chrome Web Store, Edge Add-ons, and Firefox AMO through extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI, it defaults to a dry run and store credentials are never tool arguments. After a real submission, `extension_store_status` reads the recorded outcome, per-store credential health, and review state from the project's public registry, so agents and CI can answer "was it approved?" without a console visit. Access tokens live at most 7 days; CI pipelines re-mint them from the console's Access tokens page.
131
131
 
132
132
  ## The extension.dev stack
133
133
 
package/dist/module.js CHANGED
@@ -193,6 +193,14 @@ __webpack_require__.d(storage_namespaceObject, {
193
193
  handler: ()=>storage_handler,
194
194
  schema: ()=>storage_schema
195
195
  });
196
+ var store_status_namespaceObject = {};
197
+ __webpack_require__.r(store_status_namespaceObject);
198
+ __webpack_require__.d(store_status_namespaceObject, {
199
+ handler: ()=>store_status_handler,
200
+ latestSubmissionsByStore: ()=>latestSubmissionsByStore,
201
+ normalizeStoresStatus: ()=>normalizeStoresStatus,
202
+ schema: ()=>store_status_schema
203
+ });
196
204
  var uninstall_browser_namespaceObject = {};
197
205
  __webpack_require__.r(uninstall_browser_namespaceObject);
198
206
  __webpack_require__.d(uninstall_browser_namespaceObject, {
@@ -211,7 +219,7 @@ __webpack_require__.d(whoami_namespaceObject, {
211
219
  handler: ()=>whoami_handler,
212
220
  schema: ()=>whoami_schema
213
221
  });
214
- var package_namespaceObject = JSON.parse('{"rE":"5.5.0","El":{"OP":"^4.0.13"}}');
222
+ var package_namespaceObject = JSON.parse('{"rE":"5.5.1","El":{"OP":"^4.0.13"}}');
215
223
  function scaffoldEnginePin(projectPath) {
216
224
  try {
217
225
  const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
@@ -4686,7 +4694,117 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
4686
4694
  if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
4687
4695
  return creds;
4688
4696
  }
4697
+ const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
4698
+ const CONSOLE_BASE = "https://console.extension.dev";
4699
+ function registryBase() {
4700
+ const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
4701
+ return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
4702
+ }
4703
+ function resolveProjectRef(overrides) {
4704
+ const workspace = String(overrides?.workspace || "").trim();
4705
+ const project = String(overrides?.project || "").trim();
4706
+ if (workspace && project) return {
4707
+ workspace,
4708
+ project
4709
+ };
4710
+ const creds = readCredentials();
4711
+ const ws = workspace || String(creds?.workspaceSlug || "").trim();
4712
+ const proj = project || String(creds?.projectSlug || "").trim();
4713
+ if (!ws || !proj) return null;
4714
+ return {
4715
+ workspace: ws,
4716
+ project: proj
4717
+ };
4718
+ }
4719
+ function registryFileUrl(ref, file) {
4720
+ return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
4721
+ }
4722
+ function consoleProjectUrl(ref, page) {
4723
+ if (!ref) return `${CONSOLE_BASE}`;
4724
+ return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
4725
+ }
4726
+ async function fetchRegistryJson(url, fetchImpl = fetch) {
4727
+ let res;
4728
+ try {
4729
+ res = await fetchImpl(url);
4730
+ } catch (err) {
4731
+ return {
4732
+ ok: false,
4733
+ message: `Could not reach ${url}: ${err?.message || err}`
4734
+ };
4735
+ }
4736
+ if (!res.ok) return {
4737
+ ok: false,
4738
+ status: res.status,
4739
+ message: `${url} returned ${res.status}`
4740
+ };
4741
+ try {
4742
+ const text = await res.text();
4743
+ return {
4744
+ ok: true,
4745
+ json: JSON.parse(text)
4746
+ };
4747
+ } catch {
4748
+ return {
4749
+ ok: false,
4750
+ message: `${url} did not return valid JSON`
4751
+ };
4752
+ }
4753
+ }
4754
+ function parseChannels(json) {
4755
+ if (!json || "object" != typeof json || Array.isArray(json)) return [];
4756
+ const out = [];
4757
+ for (const [channel, raw] of Object.entries(json)){
4758
+ if (!raw || "object" != typeof raw) continue;
4759
+ const row = raw;
4760
+ const description = "string" == typeof row.description ? row.description : void 0;
4761
+ const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
4762
+ const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
4763
+ const entry = {
4764
+ channel,
4765
+ sha: String(row.sha ?? "")
4766
+ };
4767
+ if (row.buildId) entry.buildId = String(row.buildId);
4768
+ if (row.version) entry.version = String(row.version);
4769
+ const promotedAt = promotedAtField || fromDescription;
4770
+ if (promotedAt) entry.promotedAt = promotedAt;
4771
+ if (description) entry.description = description;
4772
+ out.push(entry);
4773
+ }
4774
+ return out;
4775
+ }
4776
+ function parseBuildIndex(json) {
4777
+ const items = json?.items;
4778
+ if (!Array.isArray(items)) return [];
4779
+ const out = [];
4780
+ for (const raw of items){
4781
+ if (!raw || "object" != typeof raw) continue;
4782
+ const row = raw;
4783
+ const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
4784
+ if (!sha) continue;
4785
+ const entry = {
4786
+ sha
4787
+ };
4788
+ if (row.commit) entry.commit = String(row.commit);
4789
+ if (row.channel) entry.channel = String(row.channel);
4790
+ if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
4791
+ if (row.status) entry.status = String(row.status);
4792
+ if (row.version) entry.version = String(row.version);
4793
+ if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
4794
+ if (row.timestamp) entry.timestamp = String(row.timestamp);
4795
+ if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
4796
+ out.push(entry);
4797
+ }
4798
+ return out;
4799
+ }
4689
4800
  const DEFAULT_API = "https://www.extension.dev";
4801
+ function tokenTtlNote(workspaceSlug, projectSlug) {
4802
+ const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
4803
+ workspace: workspaceSlug,
4804
+ project: projectSlug
4805
+ }, "settings/access-tokens") : CONSOLE_BASE;
4806
+ return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
4807
+ }
4690
4808
  function resolveApiBase(api) {
4691
4809
  return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
4692
4810
  }
@@ -4851,109 +4969,6 @@ async function publish(options = {}) {
4851
4969
  data
4852
4970
  };
4853
4971
  }
4854
- const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
4855
- const CONSOLE_BASE = "https://console.extension.dev";
4856
- function registryBase() {
4857
- const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
4858
- return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
4859
- }
4860
- function resolveProjectRef(overrides) {
4861
- const workspace = String(overrides?.workspace || "").trim();
4862
- const project = String(overrides?.project || "").trim();
4863
- if (workspace && project) return {
4864
- workspace,
4865
- project
4866
- };
4867
- const creds = readCredentials();
4868
- const ws = workspace || String(creds?.workspaceSlug || "").trim();
4869
- const proj = project || String(creds?.projectSlug || "").trim();
4870
- if (!ws || !proj) return null;
4871
- return {
4872
- workspace: ws,
4873
- project: proj
4874
- };
4875
- }
4876
- function registryFileUrl(ref, file) {
4877
- return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
4878
- }
4879
- function consoleProjectUrl(ref, page) {
4880
- if (!ref) return `${CONSOLE_BASE}`;
4881
- return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
4882
- }
4883
- async function fetchRegistryJson(url, fetchImpl = fetch) {
4884
- let res;
4885
- try {
4886
- res = await fetchImpl(url);
4887
- } catch (err) {
4888
- return {
4889
- ok: false,
4890
- message: `Could not reach ${url}: ${err?.message || err}`
4891
- };
4892
- }
4893
- if (!res.ok) return {
4894
- ok: false,
4895
- status: res.status,
4896
- message: `${url} returned ${res.status}`
4897
- };
4898
- try {
4899
- const text = await res.text();
4900
- return {
4901
- ok: true,
4902
- json: JSON.parse(text)
4903
- };
4904
- } catch {
4905
- return {
4906
- ok: false,
4907
- message: `${url} did not return valid JSON`
4908
- };
4909
- }
4910
- }
4911
- function parseChannels(json) {
4912
- if (!json || "object" != typeof json || Array.isArray(json)) return [];
4913
- const out = [];
4914
- for (const [channel, raw] of Object.entries(json)){
4915
- if (!raw || "object" != typeof raw) continue;
4916
- const row = raw;
4917
- const description = "string" == typeof row.description ? row.description : void 0;
4918
- const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
4919
- const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
4920
- const entry = {
4921
- channel,
4922
- sha: String(row.sha ?? "")
4923
- };
4924
- if (row.buildId) entry.buildId = String(row.buildId);
4925
- if (row.version) entry.version = String(row.version);
4926
- const promotedAt = promotedAtField || fromDescription;
4927
- if (promotedAt) entry.promotedAt = promotedAt;
4928
- if (description) entry.description = description;
4929
- out.push(entry);
4930
- }
4931
- return out;
4932
- }
4933
- function parseBuildIndex(json) {
4934
- const items = json?.items;
4935
- if (!Array.isArray(items)) return [];
4936
- const out = [];
4937
- for (const raw of items){
4938
- if (!raw || "object" != typeof raw) continue;
4939
- const row = raw;
4940
- const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
4941
- if (!sha) continue;
4942
- const entry = {
4943
- sha
4944
- };
4945
- if (row.commit) entry.commit = String(row.commit);
4946
- if (row.channel) entry.channel = String(row.channel);
4947
- if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
4948
- if (row.status) entry.status = String(row.status);
4949
- if (row.version) entry.version = String(row.version);
4950
- if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
4951
- if (row.timestamp) entry.timestamp = String(row.timestamp);
4952
- if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
4953
- out.push(entry);
4954
- }
4955
- return out;
4956
- }
4957
4972
  const publish_schema = {
4958
4973
  name: "extension_publish",
4959
4974
  description: "Publish the project your stored token is scoped to (from extension_login, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. The publish target is the token's project -- there is no projectPath, the local files are not uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE project it is a fresh time-limited share link (?share=) whose lifetime is ttlHours. Posts to the platform's CLI publish endpoint. Besides extension_login this is the only tool that talks to the hosted platform.",
@@ -5031,7 +5046,7 @@ async function publish_handler(args) {
5031
5046
  const release_promote_DEFAULT_API = "https://www.extension.dev";
5032
5047
  const release_promote_schema = {
5033
5048
  name: "extension_release_promote",
5034
- description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
5049
+ description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
5035
5050
  inputSchema: {
5036
5051
  type: "object",
5037
5052
  properties: {
@@ -5084,7 +5099,7 @@ function release_promote_fail(name, message) {
5084
5099
  }
5085
5100
  async function release_promote_handler(args) {
5086
5101
  const token = resolveToken();
5087
- if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
5102
+ if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_login.");
5088
5103
  const buildId = String(args.buildId || "").trim();
5089
5104
  const channel = String(args.channel || "").trim();
5090
5105
  if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
@@ -5236,7 +5251,7 @@ function storeMdWarnings(browsers, cwd) {
5236
5251
  content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
5237
5252
  } catch {
5238
5253
  return [
5239
- "No STORE.md found in the project root. Reviewer notes (Firefox) and certification notes (Edge) will not accompany the submission. See the extension-dev skill's store-md reference."
5254
+ "No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
5240
5255
  ];
5241
5256
  }
5242
5257
  const hasField = (section, field)=>{
@@ -5255,7 +5270,7 @@ function storeMdWarnings(browsers, cwd) {
5255
5270
  }
5256
5271
  const deploy_schema = {
5257
5272
  name: "extension_deploy",
5258
- description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight: verifies auth, the project, that the build exists, and the store workflow - dispatches nothing); pass dryRun:false to actually submit, which is irreversible and enters store review. The target project is identified by your token (extension_login or a release token in EXTENSION_DEV_TOKEN); store credentials are never tool arguments and local files are not uploaded. Pass browsers + buildSha. Posts to the platform's CLI store-submission endpoint.",
5273
+ description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight - dispatches nothing): the platform side verifies auth, the project, that the build exists, and the store workflow, and this tool then adds the per-store verdict from each store's public credential-health record; trust the per-store rows in the result over the platform's bare preflight line, which does not check store health. Pass dryRun:false to actually submit, which is irreversible and enters store review. The target project is identified by your token (extension_login or a release token in EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page). Store credentials are never tool arguments and local files are not uploaded. Pass browsers + buildSha (extension_release_list lists valid shas); after a real submission, extension_store_status reads the recorded outcome and review state. Posts to the platform's CLI store-submission endpoint.",
5259
5274
  inputSchema: {
5260
5275
  type: "object",
5261
5276
  properties: {
@@ -5311,7 +5326,7 @@ function deploy_fail(name, message) {
5311
5326
  }
5312
5327
  async function deploy_handler(args) {
5313
5328
  const token = resolveToken();
5314
- if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens).");
5329
+ if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).");
5315
5330
  const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
5316
5331
  if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
5317
5332
  const buildSha = String(args.buildSha || "").trim();
@@ -5429,9 +5444,214 @@ async function deploy_handler(args) {
5429
5444
  if ("string" == typeof data?.message) result.platformMessage = data.message;
5430
5445
  result.message = summaryParts.join(" ");
5431
5446
  }
5447
+ if (!dryRun) result.statusNote = "Track this submission with extension_store_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
5432
5448
  if (warnings.length > 0) result.warnings = warnings;
5433
5449
  return JSON.stringify(result);
5434
5450
  }
5451
+ const KNOWN_STORES = [
5452
+ "chrome",
5453
+ "firefox",
5454
+ "edge",
5455
+ "safari"
5456
+ ];
5457
+ const store_status_schema = {
5458
+ name: "extension_store_status",
5459
+ description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge, safari) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
5460
+ inputSchema: {
5461
+ type: "object",
5462
+ properties: {
5463
+ workspace: {
5464
+ type: "string",
5465
+ description: "Workspace slug override (defaults to the stored login's workspace)."
5466
+ },
5467
+ project: {
5468
+ type: "string",
5469
+ description: "Project slug override (defaults to the stored login's project)."
5470
+ }
5471
+ },
5472
+ required: []
5473
+ }
5474
+ };
5475
+ function isPlainObject(value) {
5476
+ return Boolean(value) && "object" == typeof value && !Array.isArray(value);
5477
+ }
5478
+ function str(value) {
5479
+ const text = String(value ?? "").trim();
5480
+ return text || void 0;
5481
+ }
5482
+ function submissionView(row) {
5483
+ const view = {};
5484
+ const version = str(row.version);
5485
+ const status = str(row.status);
5486
+ const storeUrl = str(row.storeUrl);
5487
+ const submittedAt = str(row.submittedAt) || str(row.timestamp);
5488
+ const channel = str(row.channel);
5489
+ const buildSha = str(row.buildSha) || str(row.buildId);
5490
+ const storeSubmissionId = str(row.storeSubmissionId);
5491
+ const failureReason = str(row.failureReason);
5492
+ if (version) view.version = version;
5493
+ if (status) view.status = status;
5494
+ if (storeUrl) view.storeUrl = storeUrl;
5495
+ if (submittedAt) view.submittedAt = submittedAt;
5496
+ if (channel) view.channel = channel;
5497
+ if (buildSha) view.buildSha = buildSha;
5498
+ if (storeSubmissionId) view.storeSubmissionId = storeSubmissionId;
5499
+ if (failureReason) view.failureReason = failureReason;
5500
+ return view;
5501
+ }
5502
+ function latestSubmissionsByStore(json) {
5503
+ const list = json?.submissions;
5504
+ const out = {};
5505
+ for (const raw of Array.isArray(list) ? list : []){
5506
+ if (!isPlainObject(raw)) continue;
5507
+ const store = str(raw.store);
5508
+ if (!store) continue;
5509
+ const at = Date.parse(String(raw.submittedAt || raw.updatedAt || ""));
5510
+ const stamp = Number.isFinite(at) ? at : 0;
5511
+ if (!out[store] || stamp >= out[store].at) out[store] = {
5512
+ at: stamp,
5513
+ view: submissionView(raw)
5514
+ };
5515
+ }
5516
+ const flat = {};
5517
+ for (const [store, entry] of Object.entries(out))flat[store] = entry.view;
5518
+ return flat;
5519
+ }
5520
+ function normalizeStoresStatus(json) {
5521
+ const base = isPlainObject(json) ? json : {};
5522
+ const out = {
5523
+ reviews: {}
5524
+ };
5525
+ if (isPlainObject(base.lastSubmission)) out.lastSubmission = base.lastSubmission;
5526
+ if (isPlainObject(base.lastOverride)) out.lastOverride = base.lastOverride;
5527
+ if (isPlainObject(base.reviews)) {
5528
+ for (const [store, raw] of Object.entries(base.reviews))if (isPlainObject(raw)) out.reviews[store] = {
5529
+ status: str(raw.status),
5530
+ version: str(raw.version),
5531
+ buildId: str(raw.buildId),
5532
+ checkedAt: str(raw.checkedAt)
5533
+ };
5534
+ }
5535
+ const lastPoll = isPlainObject(base.lastPoll) ? base.lastPoll : null;
5536
+ const looksLikeLegacyPoll = !lastPoll && ("store_status" === base.kind || Array.isArray(base.updates));
5537
+ if (lastPoll) out.lastPollAt = str(lastPoll.timestamp);
5538
+ else if (looksLikeLegacyPoll) {
5539
+ out.lastPollAt = str(base.updatedAt);
5540
+ for (const raw of Array.isArray(base.updates) ? base.updates : []){
5541
+ if (!isPlainObject(raw)) continue;
5542
+ const store = str(raw.store);
5543
+ if (store && !out.reviews[store]) out.reviews[store] = {
5544
+ status: str(raw.status),
5545
+ version: str(raw.version),
5546
+ buildId: str(raw.buildId),
5547
+ checkedAt: str(base.updatedAt)
5548
+ };
5549
+ }
5550
+ }
5551
+ return out;
5552
+ }
5553
+ function store_status_fail(name, message, extra) {
5554
+ return JSON.stringify({
5555
+ ok: false,
5556
+ error: {
5557
+ name,
5558
+ message
5559
+ },
5560
+ ...extra ?? {}
5561
+ });
5562
+ }
5563
+ async function store_status_handler(args) {
5564
+ const ref = resolveProjectRef(args);
5565
+ if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_login (the stored login names the project), or pass workspace + project explicitly.");
5566
+ const healthUrl = registryFileUrl(ref, "stores/health.json");
5567
+ const statusUrl = registryFileUrl(ref, "stores/status.json");
5568
+ const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
5569
+ const consoleStoresUrl = consoleProjectUrl(ref, "stores");
5570
+ const [healthRes, statusRes, submissionsRes] = await Promise.all([
5571
+ fetchRegistryJson(healthUrl),
5572
+ fetchRegistryJson(statusUrl),
5573
+ fetchRegistryJson(submissionsUrl)
5574
+ ]);
5575
+ if (!healthRes.ok && !statusRes.ok && !submissionsRes.ok) return store_status_fail("StoreStatusNotFound", `No store data on the registry for ${ref.workspace}/${ref.project} (${healthUrl} returned ${healthRes.status ?? "no response"}). The project may have no stores configured yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. Configure stores at ${consoleStoresUrl}/new; the console Stores page is the authoritative view: ${consoleStoresUrl}`, {
5576
+ workspace: ref.workspace,
5577
+ project: ref.project,
5578
+ registryUrls: {
5579
+ health: healthUrl,
5580
+ status: statusUrl,
5581
+ submissions: submissionsUrl
5582
+ },
5583
+ consoleStoresUrl
5584
+ });
5585
+ const healthStores = healthRes.ok ? isPlainObject(healthRes.json?.stores) ? healthRes.json.stores ?? null : null : null;
5586
+ const status = normalizeStoresStatus(statusRes.ok ? statusRes.json : null);
5587
+ const submissionsByStore = submissionsRes.ok ? latestSubmissionsByStore(submissionsRes.json) : {};
5588
+ const statusLastStore = str(status.lastSubmission?.store);
5589
+ if (statusLastStore && !submissionsByStore[statusLastStore] && status.lastSubmission) submissionsByStore[statusLastStore] = submissionView(status.lastSubmission);
5590
+ const stores = [
5591
+ ...KNOWN_STORES,
5592
+ ...Object.keys({
5593
+ ...healthStores,
5594
+ ...submissionsByStore,
5595
+ ...status.reviews
5596
+ }).filter((s)=>!KNOWN_STORES.includes(s))
5597
+ ];
5598
+ const rows = stores.map((store)=>{
5599
+ const healthRow = healthStores?.[store];
5600
+ const configured = healthStores ? Boolean(healthRow) : "unknown";
5601
+ const row = {
5602
+ store,
5603
+ configured
5604
+ };
5605
+ if (healthRow) row.health = {
5606
+ ok: true === healthRow.ok,
5607
+ checkedAt: str(healthRow.checkedAt),
5608
+ message: str(healthRow.message)
5609
+ };
5610
+ const submission = submissionsByStore[store];
5611
+ if (submission && Object.keys(submission).length > 0) row.lastSubmission = submission;
5612
+ const review = status.reviews[store];
5613
+ if (review && Object.values(review).some(Boolean)) row.review = review;
5614
+ return row;
5615
+ });
5616
+ const summaryParts = rows.map((row)=>{
5617
+ const store = String(row.store);
5618
+ const health = row.health;
5619
+ const submission = row.lastSubmission;
5620
+ const review = row.review;
5621
+ let head;
5622
+ head = "unknown" === row.configured ? `${store}: configuration unknown (stores/health.json is unreadable)` : false === row.configured ? `${store}: not configured (add it at ${consoleStoresUrl}/new)` : health && !health.ok ? `${store}: configured but its credentials FAILED the last health check (${health.message || "no reason recorded"}) - fix them at ${consoleStoresUrl}/${store}` : `${store}: configured, credentials healthy`;
5623
+ const tail = [];
5624
+ if (submission) {
5625
+ tail.push(`last submission${submission.version ? ` v${submission.version}` : ""} ${submission.status || "recorded"}${submission.submittedAt ? ` at ${submission.submittedAt}` : ""}${submission.failureReason ? ` (${submission.failureReason})` : ""}`);
5626
+ if (submission.storeUrl) tail.push(`listing ${submission.storeUrl}`);
5627
+ } else if (true === row.configured) tail.push("no submissions recorded");
5628
+ if (review?.status) tail.push(`review ${review.status}${review.checkedAt ? ` (checked ${review.checkedAt})` : ""}`);
5629
+ return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
5630
+ });
5631
+ const result = {
5632
+ ok: true,
5633
+ workspace: ref.workspace,
5634
+ project: ref.project,
5635
+ stores: rows,
5636
+ ...status.lastSubmission ? {
5637
+ lastSubmission: status.lastSubmission
5638
+ } : {},
5639
+ ...status.lastPollAt ? {
5640
+ lastPollAt: status.lastPollAt
5641
+ } : {},
5642
+ registryUrls: {
5643
+ health: healthUrl,
5644
+ status: statusUrl,
5645
+ submissions: submissionsUrl
5646
+ },
5647
+ consoleStoresUrl,
5648
+ message: `${summaryParts.join(". ")}. This is the registry's recorded state (submissions and the review poller write it); the store dashboards are authoritative and may be ahead of it.`
5649
+ };
5650
+ if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
5651
+ if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
5652
+ if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
5653
+ return JSON.stringify(result);
5654
+ }
5435
5655
  function doctor_readReadyContract(projectPath, browser) {
5436
5656
  try {
5437
5657
  const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
@@ -6187,7 +6407,7 @@ async function pollDeviceToken(args) {
6187
6407
  }
6188
6408
  const login_schema = {
6189
6409
  name: "extension_login",
6190
- description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
6410
+ description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
6191
6411
  inputSchema: {
6192
6412
  type: "object",
6193
6413
  properties: {
@@ -6221,13 +6441,15 @@ function login_fail(name, message) {
6221
6441
  });
6222
6442
  }
6223
6443
  function success(creds) {
6444
+ const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
6224
6445
  return JSON.stringify({
6225
6446
  ok: true,
6226
6447
  status: "logged-in",
6227
6448
  workspaceSlug: creds.workspaceSlug,
6228
6449
  projectSlug: creds.projectSlug,
6229
- expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
6230
- message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token.`
6450
+ expiresAt,
6451
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
6452
+ message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token. The token expires ${expiresAt ?? "within 7 days"}: extension.dev CLI tokens live at most 7 days, so CI must re-mint before then (console: project settings -> Access tokens).`
6231
6453
  });
6232
6454
  }
6233
6455
  function login_pending(start) {
@@ -6243,6 +6465,7 @@ function login_pending(start) {
6243
6465
  verificationUriComplete: complete
6244
6466
  } : {},
6245
6467
  deviceCode: start.deviceCode,
6468
+ tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
6246
6469
  message
6247
6470
  });
6248
6471
  }
@@ -6252,6 +6475,7 @@ function resumePending(deviceCode, verificationUri) {
6252
6475
  status: "authorization_pending",
6253
6476
  verificationUri,
6254
6477
  deviceCode,
6478
+ tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
6255
6479
  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_login again with this same deviceCode (and the same project).`
6256
6480
  });
6257
6481
  }
@@ -6361,7 +6585,7 @@ async function login_handler(args) {
6361
6585
  }
6362
6586
  const whoami_schema = {
6363
6587
  name: "extension_whoami",
6364
- description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. Returns logged-out status when no credentials are stored.",
6588
+ description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. The result records where the login was minted (apiRecordedAtLogin) without asserting a platform base URL: authenticated tools target their own api argument, EXTENSION_DEV_API_URL, or the production default. Returns logged-out status when no credentials are stored.",
6365
6589
  inputSchema: {
6366
6590
  type: "object",
6367
6591
  properties: {}
@@ -6376,17 +6600,32 @@ async function whoami_handler() {
6376
6600
  });
6377
6601
  const now = Math.floor(Date.now() / 1000);
6378
6602
  const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
6603
+ const recordedApi = String(creds.api || "").trim();
6604
+ const effectiveDefaultApi = resolveApiBase();
6605
+ const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
6606
+ const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
6607
+ const messageParts = [];
6608
+ messageParts.push(expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`);
6609
+ if (apiDiverges) messageParts.push(`This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.`);
6610
+ if (envTokenSet) messageParts.push("EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login.");
6379
6611
  return JSON.stringify({
6380
6612
  ok: true,
6381
6613
  status: expired ? "expired" : "logged-in",
6382
6614
  workspaceSlug: creds.workspaceSlug,
6383
6615
  projectSlug: creds.projectSlug,
6384
- api: creds.api,
6616
+ ...recordedApi ? {
6617
+ apiRecordedAtLogin: recordedApi
6618
+ } : {},
6619
+ apiDefault: effectiveDefaultApi,
6385
6620
  provider: creds.provider ?? "github",
6386
6621
  expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
6387
6622
  expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
6388
6623
  expired,
6389
- message: expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`
6624
+ ...envTokenSet ? {
6625
+ envTokenOverride: true
6626
+ } : {},
6627
+ tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
6628
+ message: messageParts.join(" ")
6390
6629
  });
6391
6630
  }
6392
6631
  const logout_schema = {
@@ -7073,6 +7312,7 @@ const tools = [
7073
7312
  release_list_namespaceObject,
7074
7313
  release_promote_namespaceObject,
7075
7314
  deploy_namespaceObject,
7315
+ store_status_namespaceObject,
7076
7316
  wait_namespaceObject,
7077
7317
  add_feature_namespaceObject,
7078
7318
  login_namespaceObject,
@@ -1,4 +1,12 @@
1
1
  import { type StoredCredentials } from "./credentials";
2
+ /**
3
+ * The platform clamps CLI token TTL to at most 7 days (server-owned; this
4
+ * client only states the truth). Every token surface repeats it so CI authors
5
+ * learn the rotation contract up front instead of from a 401 a week after
6
+ * wiring the pipeline. Links the console's Access tokens page when the
7
+ * project is known.
8
+ */
9
+ export declare function tokenTtlNote(workspaceSlug?: string, projectSlug?: string): string;
2
10
  type FetchImpl = typeof fetch;
3
11
  export declare function resolveApiBase(api?: string): string;
4
12
  /**
@@ -0,0 +1,56 @@
1
+ export declare const schema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ workspace: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ project: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ };
16
+ required: never[];
17
+ };
18
+ };
19
+ interface SubmissionView {
20
+ version?: string;
21
+ status?: string;
22
+ storeUrl?: string;
23
+ submittedAt?: string;
24
+ channel?: string;
25
+ buildSha?: string;
26
+ storeSubmissionId?: string;
27
+ failureReason?: string;
28
+ }
29
+ interface ReviewView {
30
+ status?: string;
31
+ version?: string;
32
+ buildId?: string;
33
+ checkedAt?: string;
34
+ }
35
+ /** Latest submissions.json record per store, newest submittedAt first. */
36
+ export declare function latestSubmissionsByStore(json: unknown): Record<string, SubmissionView>;
37
+ export interface NormalizedStoresStatus {
38
+ lastSubmission?: Record<string, unknown>;
39
+ lastOverride?: Record<string, unknown>;
40
+ reviews: Record<string, ReviewView>;
41
+ lastPollAt?: string;
42
+ }
43
+ /**
44
+ * Accept any historical shape of stores/status.json. SchemaVersion 3 carries
45
+ * lastSubmission / lastOverride / lastPoll / reviews as sections. Legacy v2
46
+ * came in two flavors that used to clobber each other: the submit-path shape
47
+ * (kind "stores_status", lastSubmission only) and the poller shape (kind
48
+ * "store_status" with perStore/updates at the top level) whose updates seed
49
+ * the reviews map here, mirroring the registry template's normalizer.
50
+ */
51
+ export declare function normalizeStoresStatus(json: unknown): NormalizedStoresStatus;
52
+ export declare function handler(args: {
53
+ workspace?: string;
54
+ project?: string;
55
+ }): Promise<string>;
56
+ export {};
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "5.5.0",
5
- "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 32 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": "5.5.1",
5
+ "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 33 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": "MIT",
8
8
  "author": {
9
9
  "name": "Cezar Augusto",
10
- "email": "boss@cezaraugusto.net",
10
+ "email": "hello@extension.dev",
11
11
  "url": "https://cezaraugusto.com"
12
12
  },
13
13
  "repository": {
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. 32 tools, 11 browsers.",
4
+ "description": "Build, run, inspect, and publish browser extensions from any MCP client. 33 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": "5.5.0",
10
+ "version": "5.5.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@extension.dev/mcp",
16
- "version": "5.5.0",
16
+ "version": "5.5.1",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"