@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +41 -2
- package/README.md +2 -2
- package/dist/module.js +355 -115
- package/dist/src/lib/login-flow.d.ts +8 -0
- package/dist/src/tools/store-status.d.ts +56 -0
- package/package.json +3 -3
- package/server.json +3 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "extensiondev-mcp",
|
|
3
3
|
"owner": {
|
|
4
4
|
"name": "Cezar Augusto",
|
|
5
|
-
"email": "
|
|
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.
|
|
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.
|
|
4
|
+
"version": "5.5.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
|
-
"email": "
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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.
|
|
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.
|
|
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
|
|
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 -
|
|
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
|
|
6230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
5
|
-
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions.
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
+
"version": "5.5.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|