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