@extension.dev/mcp 6.2.0 → 6.3.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 +64 -0
- package/dist/module.js +549 -141
- package/dist/src/lib/registry-access.d.ts +45 -0
- package/dist/src/lib/registry.d.ts +31 -4
- package/dist/src/tools/release-list.d.ts +5 -0
- package/dist/src/tools/store-status.d.ts +5 -0
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -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.3.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.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "hello@extension.dev",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.4.0
|
|
4
|
+
|
|
5
|
+
A build on your machine can now become a link someone else can open.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **`extension_preview_web` with `share:true` uploads the build you just
|
|
10
|
+
made.** It used to route sharing through `extension_publish`, which takes no
|
|
11
|
+
project path and uploads nothing: it flips a share link on whatever build the
|
|
12
|
+
token's project last pushed from its own CI. So the link rendered a different
|
|
13
|
+
build than the one on disk, and said so in a caveat. It now POSTs the resolved
|
|
14
|
+
`dist/<browser>` to the platform's artifact store and returns a
|
|
15
|
+
`preview.extension.dev` link that renders those exact bytes, for a recipient
|
|
16
|
+
with no install, no sign-in, and no dev server. `share.serves` reports
|
|
17
|
+
`uploaded-local-build`; the `requiredBackend` spec that described this as
|
|
18
|
+
missing is gone because it is built.
|
|
19
|
+
- `share` now returns `expiresAt` and `revokeUrl`. Shared builds expire, and
|
|
20
|
+
`DELETE`ing `revokeUrl` with the same token kills the link immediately, which
|
|
21
|
+
is the part a TTL alone cannot do when a link reaches the wrong person.
|
|
22
|
+
- The `channel` argument is removed from `extension_preview_web`. It only ever
|
|
23
|
+
selected which promoted CI build to render, and this tool no longer renders
|
|
24
|
+
promoted builds. `extension_publish` and `extension_release_promote` are
|
|
25
|
+
unchanged and remain the path for released, channel-scoped builds.
|
|
26
|
+
|
|
27
|
+
## 6.3.0
|
|
28
|
+
|
|
29
|
+
Private projects stop reading as empty, and the links this server hands back
|
|
30
|
+
stop being login-only.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Registry reads work for PRIVATE projects.** `extension_release_list`,
|
|
35
|
+
`extension_store_status`, `extension_deploy` and `extension_publish` read the
|
|
36
|
+
project's state from `registry.extension.land`, which answers 401 for a
|
|
37
|
+
private project. That 401 was reported as "no registry data", so a project the
|
|
38
|
+
operator owns and is logged into looked like it had no builds at all. A 401 or
|
|
39
|
+
403 now mints a short-lived read token and retries once. Public projects are
|
|
40
|
+
untouched: still one request, still no call to the platform.
|
|
41
|
+
- The stored login token is never used as the `?t=` URL parameter even though it
|
|
42
|
+
would verify. It is long-lived, and a credential in a query string is kept by
|
|
43
|
+
every proxy and access log on the path, so it is traded for a ten-minute token
|
|
44
|
+
first. Requires the bearer path on `POST /api/access-grant`.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- **Public build links.** `extension_release_list` returns a `publicUrl` for
|
|
49
|
+
every channel and build plus a `publicProjectUrl`, and
|
|
50
|
+
`extension_release_promote` returns `publicChannelUrl` and `publicBuildUrl`.
|
|
51
|
+
Every link these tools returned before pointed at the console, which requires
|
|
52
|
+
a login and workspace membership, so it was a dead end for the teammate or
|
|
53
|
+
reviewer the operator wanted to send it to. The public pages carry the
|
|
54
|
+
per-browser downloads, the run-locally and integrity dialogs, and what's new.
|
|
55
|
+
For a private project the response says plainly that an outside recipient
|
|
56
|
+
still needs a share link from `extension_publish`.
|
|
57
|
+
- `api` is accepted by `extension_release_list` and `extension_store_status`,
|
|
58
|
+
matching the other hosted-facing tools. It picks the platform the read token
|
|
59
|
+
is minted against and the origin the returned links point at.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- Depends on `@extension.dev/urls` `^0.3.0` for the new `userland` origin and
|
|
64
|
+
its whole-URL builders, so a build link this server returns cannot drift from
|
|
65
|
+
the routes the viewer serves.
|
|
66
|
+
|
|
3
67
|
## 6.2.0
|
|
4
68
|
|
|
5
69
|
The URL layer stops being a copy. This server used to carry byte-identical
|
package/dist/module.js
CHANGED
|
@@ -17,7 +17,7 @@ import node_net from "node:net";
|
|
|
17
17
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
__webpack_require__.add({
|
|
20
|
-
"./node_modules/.pnpm/@extension.dev+urls@0.
|
|
20
|
+
"./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/origins.cjs" (__unused_rspack_module, exports) {
|
|
21
21
|
var __nested_rspack_require_18_37__ = {};
|
|
22
22
|
(()=>{
|
|
23
23
|
__nested_rspack_require_18_37__.d = (exports1, definition)=>{
|
|
@@ -55,6 +55,7 @@ __webpack_require__.add({
|
|
|
55
55
|
preview: "https://preview.extension.dev",
|
|
56
56
|
templates: "https://templates.extension.dev",
|
|
57
57
|
intelligence: "https://intelligence.extension.dev",
|
|
58
|
+
userland: "https://extension.dev",
|
|
58
59
|
registry: "https://registry.extension.land",
|
|
59
60
|
media: "https://media.extension.land"
|
|
60
61
|
};
|
|
@@ -65,6 +66,7 @@ __webpack_require__.add({
|
|
|
65
66
|
preview: "http://preview.extension.localhost",
|
|
66
67
|
templates: "http://templates.extension.localhost",
|
|
67
68
|
intelligence: "http://intelligence.extension.localhost",
|
|
69
|
+
userland: "http://userland.extension.localhost",
|
|
68
70
|
registry: "https://registry.extension.land",
|
|
69
71
|
media: "https://media.extension.land"
|
|
70
72
|
};
|
|
@@ -93,6 +95,7 @@ __webpack_require__.add({
|
|
|
93
95
|
preview: pick("preview"),
|
|
94
96
|
templates: pick("templates"),
|
|
95
97
|
intelligence: pick("intelligence"),
|
|
98
|
+
userland: pick("userland"),
|
|
96
99
|
registry: pick("registry"),
|
|
97
100
|
media: pick("media")
|
|
98
101
|
};
|
|
@@ -111,7 +114,7 @@ __webpack_require__.add({
|
|
|
111
114
|
value: true
|
|
112
115
|
});
|
|
113
116
|
},
|
|
114
|
-
"./node_modules/.pnpm/@extension.dev+urls@0.
|
|
117
|
+
"./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/paths.cjs" (__unused_rspack_module, exports) {
|
|
115
118
|
var __nested_rspack_require_18_37__ = {};
|
|
116
119
|
(()=>{
|
|
117
120
|
__nested_rspack_require_18_37__.d = (exports1, definition)=>{
|
|
@@ -143,6 +146,7 @@ __webpack_require__.add({
|
|
|
143
146
|
inspectTabPath: ()=>inspectTabPath,
|
|
144
147
|
previewSharePath: ()=>previewSharePath,
|
|
145
148
|
templateTabPath: ()=>templateTabPath,
|
|
149
|
+
withQuery: ()=>withQuery,
|
|
146
150
|
wwwDevicePath: ()=>wwwDevicePath,
|
|
147
151
|
wwwImportPath: ()=>wwwImportPath,
|
|
148
152
|
wwwNewPath: ()=>wwwNewPath,
|
|
@@ -212,6 +216,7 @@ __webpack_require__.add({
|
|
|
212
216
|
exports.inspectTabPath = __nested_rspack_exports__.inspectTabPath;
|
|
213
217
|
exports.previewSharePath = __nested_rspack_exports__.previewSharePath;
|
|
214
218
|
exports.templateTabPath = __nested_rspack_exports__.templateTabPath;
|
|
219
|
+
exports.withQuery = __nested_rspack_exports__.withQuery;
|
|
215
220
|
exports.wwwDevicePath = __nested_rspack_exports__.wwwDevicePath;
|
|
216
221
|
exports.wwwImportPath = __nested_rspack_exports__.wwwImportPath;
|
|
217
222
|
exports.wwwNewPath = __nested_rspack_exports__.wwwNewPath;
|
|
@@ -223,6 +228,7 @@ __webpack_require__.add({
|
|
|
223
228
|
"inspectTabPath",
|
|
224
229
|
"previewSharePath",
|
|
225
230
|
"templateTabPath",
|
|
231
|
+
"withQuery",
|
|
226
232
|
"wwwDevicePath",
|
|
227
233
|
"wwwImportPath",
|
|
228
234
|
"wwwNewPath",
|
|
@@ -231,6 +237,171 @@ __webpack_require__.add({
|
|
|
231
237
|
Object.defineProperty(exports, "__esModule", {
|
|
232
238
|
value: true
|
|
233
239
|
});
|
|
240
|
+
},
|
|
241
|
+
"./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/userland.cjs" (__unused_rspack_module, exports) {
|
|
242
|
+
var __nested_rspack_require_18_37__ = {};
|
|
243
|
+
(()=>{
|
|
244
|
+
__nested_rspack_require_18_37__.d = (exports1, definition)=>{
|
|
245
|
+
for(var key in definition)if (__nested_rspack_require_18_37__.o(definition, key) && !__nested_rspack_require_18_37__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
246
|
+
enumerable: true,
|
|
247
|
+
get: definition[key]
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
})();
|
|
251
|
+
(()=>{
|
|
252
|
+
__nested_rspack_require_18_37__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
253
|
+
})();
|
|
254
|
+
(()=>{
|
|
255
|
+
__nested_rspack_require_18_37__.r = (exports1)=>{
|
|
256
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
257
|
+
value: 'Module'
|
|
258
|
+
});
|
|
259
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
260
|
+
value: true
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
})();
|
|
264
|
+
var __nested_rspack_exports__ = {};
|
|
265
|
+
__nested_rspack_require_18_37__.r(__nested_rspack_exports__);
|
|
266
|
+
__nested_rspack_require_18_37__.d(__nested_rspack_exports__, {
|
|
267
|
+
userlandBuildUrl: ()=>userlandBuildUrl,
|
|
268
|
+
USERLAND_BUILD_TABS: ()=>USERLAND_BUILD_TABS,
|
|
269
|
+
userlandUrl: ()=>userlandUrl,
|
|
270
|
+
UserlandProjectPage: ()=>UserlandProjectPage,
|
|
271
|
+
UserlandDialog: ()=>UserlandDialog,
|
|
272
|
+
userlandChannelUrl: ()=>userlandChannelUrl,
|
|
273
|
+
userlandProjectPath: ()=>userlandProjectPath,
|
|
274
|
+
isUserlandBuildTab: ()=>isUserlandBuildTab,
|
|
275
|
+
userlandDialogUrl: ()=>userlandDialogUrl,
|
|
276
|
+
userlandHostMode: ()=>userlandHostMode,
|
|
277
|
+
userlandOrigin: ()=>userlandOrigin
|
|
278
|
+
});
|
|
279
|
+
const PROD_ORIGINS = {
|
|
280
|
+
www: "https://www.extension.dev",
|
|
281
|
+
console: "https://console.extension.dev",
|
|
282
|
+
inspect: "https://inspect.extension.dev",
|
|
283
|
+
preview: "https://preview.extension.dev",
|
|
284
|
+
templates: "https://templates.extension.dev",
|
|
285
|
+
intelligence: "https://intelligence.extension.dev",
|
|
286
|
+
userland: "https://extension.dev",
|
|
287
|
+
registry: "https://registry.extension.land",
|
|
288
|
+
media: "https://media.extension.land"
|
|
289
|
+
};
|
|
290
|
+
function strip(value) {
|
|
291
|
+
return String(value ?? "").trim().replace(/\/+$/, "");
|
|
292
|
+
}
|
|
293
|
+
function isLocalOrigin(url) {
|
|
294
|
+
const raw = strip(url);
|
|
295
|
+
if (!raw) return false;
|
|
296
|
+
let host;
|
|
297
|
+
try {
|
|
298
|
+
host = new URL(raw).hostname;
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return "localhost" === host || "127.0.0.1" === host || "::1" === host || "[::1]" === host || "extension.localhost" === host || host.endsWith(".extension.localhost");
|
|
303
|
+
}
|
|
304
|
+
function withQuery(path, query) {
|
|
305
|
+
if (!query) return path;
|
|
306
|
+
const params = new URLSearchParams();
|
|
307
|
+
for (const [key, value] of Object.entries(query))if (null != value && "" !== value) params.set(key, String(value));
|
|
308
|
+
const qs = params.toString();
|
|
309
|
+
return qs ? `${path}?${qs}` : path;
|
|
310
|
+
}
|
|
311
|
+
const userland_seg = (value)=>encodeURIComponent(String(value));
|
|
312
|
+
const USERLAND_BUILD_TABS = [
|
|
313
|
+
"preview",
|
|
314
|
+
"whats-new",
|
|
315
|
+
"usage"
|
|
316
|
+
];
|
|
317
|
+
function isUserlandBuildTab(value) {
|
|
318
|
+
return USERLAND_BUILD_TABS.includes(value);
|
|
319
|
+
}
|
|
320
|
+
const UserlandDialog = {
|
|
321
|
+
run: "run",
|
|
322
|
+
integrity: "integrity"
|
|
323
|
+
};
|
|
324
|
+
const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
325
|
+
function userland_strip(value) {
|
|
326
|
+
return String(value ?? "").trim().replace(/\/+$/, "");
|
|
327
|
+
}
|
|
328
|
+
function userlandHostMode(base) {
|
|
329
|
+
return isLocalOrigin(userland_strip(base) || PROD_ORIGINS.userland) ? "path" : "subdomain";
|
|
330
|
+
}
|
|
331
|
+
function userlandOrigin(workspace, options = {}) {
|
|
332
|
+
const base = userland_strip(options.base) || PROD_ORIGINS.userland;
|
|
333
|
+
if ("path" === userlandHostMode(base)) return base;
|
|
334
|
+
const slug = String(workspace ?? "").trim().toLowerCase();
|
|
335
|
+
if (!DNS_LABEL.test(slug)) throw new Error(`Workspace slug ${JSON.stringify(workspace)} cannot be a subdomain label; userland production hosts are <workspace>.extension.dev.`);
|
|
336
|
+
const url = new URL(base);
|
|
337
|
+
url.hostname = `${slug}.${url.hostname}`;
|
|
338
|
+
return userland_strip(url.toString());
|
|
339
|
+
}
|
|
340
|
+
const UserlandProjectPage = {
|
|
341
|
+
overview: "",
|
|
342
|
+
build: (buildId)=>`builds/${userland_seg(buildId)}`,
|
|
343
|
+
buildTab: (buildId, tab = "preview")=>"preview" === tab ? `builds/${userland_seg(buildId)}` : `builds/${userland_seg(buildId)}/${tab}`,
|
|
344
|
+
browserBuild: (buildId, browser)=>`builds/${userland_seg(buildId)}/${userland_seg(browser)}`,
|
|
345
|
+
channel: (channel)=>`channels/${userland_seg(channel)}`,
|
|
346
|
+
channelShortcut: (channel)=>userland_seg(channel),
|
|
347
|
+
channelBrowser: (browser, channel)=>`channels/${userland_seg(browser)}/${userland_seg(channel)}`,
|
|
348
|
+
version: (version)=>`versions/${userland_seg(version)}`,
|
|
349
|
+
versionBrowser: (browser, version)=>`versions/${userland_seg(browser)}/${userland_seg(version)}`
|
|
350
|
+
};
|
|
351
|
+
function userlandProjectPath(ref, page = "", options = {}) {
|
|
352
|
+
const head = "subdomain" === userlandHostMode(options.base) ? `/${userland_seg(ref.project)}` : `/${userland_seg(ref.workspace)}/${userland_seg(ref.project)}`;
|
|
353
|
+
const tail = String(page ?? "").replace(/^\/+/, "");
|
|
354
|
+
return tail ? `${head}/${tail}` : head;
|
|
355
|
+
}
|
|
356
|
+
function userlandUrl(ref, page = "", options = {}) {
|
|
357
|
+
const origin = userlandOrigin(ref.workspace, options);
|
|
358
|
+
const path = userlandProjectPath(ref, page, options);
|
|
359
|
+
return `${origin}${withQuery(path, options.query)}`;
|
|
360
|
+
}
|
|
361
|
+
function userlandBuildUrl(ref, buildId, options = {}) {
|
|
362
|
+
const page = options.browser ? UserlandProjectPage.browserBuild(buildId, options.browser) : UserlandProjectPage.build(buildId);
|
|
363
|
+
return userlandUrl(ref, page, options);
|
|
364
|
+
}
|
|
365
|
+
function userlandDialogUrl(ref, buildId, browser, dialog, options = {}) {
|
|
366
|
+
return userlandUrl(ref, UserlandProjectPage.browserBuild(buildId, browser), {
|
|
367
|
+
...options,
|
|
368
|
+
query: {
|
|
369
|
+
...options.query ?? {},
|
|
370
|
+
dialog
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function userlandChannelUrl(ref, channel, options = {}) {
|
|
375
|
+
const page = options.browser ? UserlandProjectPage.channelBrowser(options.browser, channel) : options.shortcut ? UserlandProjectPage.channelShortcut(channel) : UserlandProjectPage.channel(channel);
|
|
376
|
+
return userlandUrl(ref, page, options);
|
|
377
|
+
}
|
|
378
|
+
exports.USERLAND_BUILD_TABS = __nested_rspack_exports__.USERLAND_BUILD_TABS;
|
|
379
|
+
exports.UserlandDialog = __nested_rspack_exports__.UserlandDialog;
|
|
380
|
+
exports.UserlandProjectPage = __nested_rspack_exports__.UserlandProjectPage;
|
|
381
|
+
exports.isUserlandBuildTab = __nested_rspack_exports__.isUserlandBuildTab;
|
|
382
|
+
exports.userlandBuildUrl = __nested_rspack_exports__.userlandBuildUrl;
|
|
383
|
+
exports.userlandChannelUrl = __nested_rspack_exports__.userlandChannelUrl;
|
|
384
|
+
exports.userlandDialogUrl = __nested_rspack_exports__.userlandDialogUrl;
|
|
385
|
+
exports.userlandHostMode = __nested_rspack_exports__.userlandHostMode;
|
|
386
|
+
exports.userlandOrigin = __nested_rspack_exports__.userlandOrigin;
|
|
387
|
+
exports.userlandProjectPath = __nested_rspack_exports__.userlandProjectPath;
|
|
388
|
+
exports.userlandUrl = __nested_rspack_exports__.userlandUrl;
|
|
389
|
+
for(var __webpack_i__ in __nested_rspack_exports__)if (-1 === [
|
|
390
|
+
"USERLAND_BUILD_TABS",
|
|
391
|
+
"UserlandDialog",
|
|
392
|
+
"UserlandProjectPage",
|
|
393
|
+
"isUserlandBuildTab",
|
|
394
|
+
"userlandBuildUrl",
|
|
395
|
+
"userlandChannelUrl",
|
|
396
|
+
"userlandDialogUrl",
|
|
397
|
+
"userlandHostMode",
|
|
398
|
+
"userlandOrigin",
|
|
399
|
+
"userlandProjectPath",
|
|
400
|
+
"userlandUrl"
|
|
401
|
+
].indexOf(__webpack_i__)) exports[__webpack_i__] = __nested_rspack_exports__[__webpack_i__];
|
|
402
|
+
Object.defineProperty(exports, "__esModule", {
|
|
403
|
+
value: true
|
|
404
|
+
});
|
|
234
405
|
}
|
|
235
406
|
});
|
|
236
407
|
var add_feature_namespaceObject = {};
|
|
@@ -439,7 +610,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
439
610
|
handler: ()=>whoami_handler,
|
|
440
611
|
schema: ()=>whoami_schema
|
|
441
612
|
});
|
|
442
|
-
var package_namespaceObject = JSON.parse('{"rE":"6.
|
|
613
|
+
var package_namespaceObject = JSON.parse('{"rE":"6.3.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
|
|
443
614
|
function credentialsPath() {
|
|
444
615
|
if ("win32" === process.platform) {
|
|
445
616
|
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
@@ -512,8 +683,240 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
|
512
683
|
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
513
684
|
return creds;
|
|
514
685
|
}
|
|
515
|
-
const origins = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.
|
|
516
|
-
const
|
|
686
|
+
const origins = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/origins.cjs");
|
|
687
|
+
const DEFAULT_API = origins.PROD_ORIGINS.www;
|
|
688
|
+
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
689
|
+
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
690
|
+
workspace: workspaceSlug,
|
|
691
|
+
project: projectSlug
|
|
692
|
+
}, "settings/access-tokens") : consoleBase();
|
|
693
|
+
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}`;
|
|
694
|
+
}
|
|
695
|
+
function resolveApiBase(api) {
|
|
696
|
+
return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
|
|
697
|
+
}
|
|
698
|
+
function safeApiBase(raw) {
|
|
699
|
+
let parsed;
|
|
700
|
+
try {
|
|
701
|
+
parsed = new URL(raw);
|
|
702
|
+
} catch {
|
|
703
|
+
return {
|
|
704
|
+
ok: false,
|
|
705
|
+
message: `Invalid platform URL: ${raw}`
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
|
|
709
|
+
if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
|
|
710
|
+
ok: false,
|
|
711
|
+
message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
|
|
712
|
+
};
|
|
713
|
+
return {
|
|
714
|
+
ok: true,
|
|
715
|
+
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
719
|
+
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
720
|
+
if (override) return {
|
|
721
|
+
provider: "github",
|
|
722
|
+
clientId: override,
|
|
723
|
+
scope: "read:user",
|
|
724
|
+
deviceCodeUrl: "/api/cli/device/code",
|
|
725
|
+
deviceTokenUrl: "/api/cli/device/token",
|
|
726
|
+
verificationUri: "https://github.com/login/device"
|
|
727
|
+
};
|
|
728
|
+
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
729
|
+
headers: {
|
|
730
|
+
accept: "application/json"
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
734
|
+
const data = await res.json().catch(()=>({}));
|
|
735
|
+
const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
|
|
736
|
+
const clientId = String(data.githubClientId || "").trim();
|
|
737
|
+
if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
738
|
+
return {
|
|
739
|
+
provider,
|
|
740
|
+
clientId,
|
|
741
|
+
scope: String(data.scope || "read:user"),
|
|
742
|
+
deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
|
|
743
|
+
deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
|
|
744
|
+
verificationUri: String(data.verificationUri || "https://github.com/login/device")
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function persistTokenResponse(args) {
|
|
748
|
+
const token = String(args.data.token || "").trim();
|
|
749
|
+
if (!token) throw new Error("Login returned no token.");
|
|
750
|
+
const creds = {
|
|
751
|
+
version: 1,
|
|
752
|
+
token,
|
|
753
|
+
workspaceSlug: String(args.data.workspaceSlug || ""),
|
|
754
|
+
projectSlug: String(args.data.projectSlug || ""),
|
|
755
|
+
expiresAt: Number(args.data.expiresAt || 0),
|
|
756
|
+
api: args.apiBase,
|
|
757
|
+
provider: args.provider
|
|
758
|
+
};
|
|
759
|
+
writeCredentials(creds);
|
|
760
|
+
return creds;
|
|
761
|
+
}
|
|
762
|
+
async function exchangeAndPersist(args) {
|
|
763
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
764
|
+
const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
|
|
765
|
+
method: "POST",
|
|
766
|
+
headers: {
|
|
767
|
+
"content-type": "application/json",
|
|
768
|
+
accept: "application/json"
|
|
769
|
+
},
|
|
770
|
+
body: JSON.stringify({
|
|
771
|
+
githubToken: args.githubToken,
|
|
772
|
+
project: args.project
|
|
773
|
+
})
|
|
774
|
+
});
|
|
775
|
+
const text = await res.text();
|
|
776
|
+
let data;
|
|
777
|
+
try {
|
|
778
|
+
data = JSON.parse(text);
|
|
779
|
+
} catch {
|
|
780
|
+
data = {
|
|
781
|
+
message: text
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
785
|
+
return persistTokenResponse({
|
|
786
|
+
apiBase: args.apiBase,
|
|
787
|
+
data,
|
|
788
|
+
provider: "github"
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function _define_property(obj, key, value) {
|
|
792
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
793
|
+
value: value,
|
|
794
|
+
enumerable: true,
|
|
795
|
+
configurable: true,
|
|
796
|
+
writable: true
|
|
797
|
+
});
|
|
798
|
+
else obj[key] = value;
|
|
799
|
+
return obj;
|
|
800
|
+
}
|
|
801
|
+
const REFRESH_LEAD_SECONDS = 60;
|
|
802
|
+
function cacheKey(ref) {
|
|
803
|
+
return `${ref.workspace.toLowerCase()}/${ref.project.toLowerCase()}`;
|
|
804
|
+
}
|
|
805
|
+
class RegistryAccessTokens {
|
|
806
|
+
peek(ref) {
|
|
807
|
+
const entry = this.cache.get(cacheKey(ref));
|
|
808
|
+
if (!entry || "fresh" !== entry.kind) return "";
|
|
809
|
+
return entry.expiresAt - REFRESH_LEAD_SECONDS > this.nowSeconds() ? entry.token : "";
|
|
810
|
+
}
|
|
811
|
+
async get(ref, apiHint) {
|
|
812
|
+
const key = cacheKey(ref);
|
|
813
|
+
const entry = this.cache.get(key);
|
|
814
|
+
if (entry?.kind === "pending") return entry.promise;
|
|
815
|
+
const cached = this.peek(ref);
|
|
816
|
+
if (cached) return {
|
|
817
|
+
status: "ok",
|
|
818
|
+
token: cached,
|
|
819
|
+
expiresAt: this.cache.get(key).expiresAt
|
|
820
|
+
};
|
|
821
|
+
const promise = this.mint(ref, apiHint);
|
|
822
|
+
this.cache.set(key, {
|
|
823
|
+
kind: "pending",
|
|
824
|
+
promise
|
|
825
|
+
});
|
|
826
|
+
const result = await promise;
|
|
827
|
+
if ("ok" === result.status) this.cache.set(key, {
|
|
828
|
+
kind: "fresh",
|
|
829
|
+
token: result.token,
|
|
830
|
+
expiresAt: result.expiresAt
|
|
831
|
+
});
|
|
832
|
+
else this.cache.delete(key);
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
async mint(ref, apiHint) {
|
|
836
|
+
const creds = readCredentials();
|
|
837
|
+
const token = String(process.env.EXTENSION_DEV_TOKEN || creds?.token || "").trim();
|
|
838
|
+
if (!token) return {
|
|
839
|
+
status: "no-credential"
|
|
840
|
+
};
|
|
841
|
+
if (creds?.workspaceSlug && creds?.projectSlug && !process.env.EXTENSION_DEV_TOKEN) {
|
|
842
|
+
const same = creds.workspaceSlug.toLowerCase() === ref.workspace.toLowerCase() && creds.projectSlug.toLowerCase() === ref.project.toLowerCase();
|
|
843
|
+
if (!same) return {
|
|
844
|
+
status: "no-credential"
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
const check = safeApiBase(resolveApiBase(apiHint || creds?.api));
|
|
848
|
+
if (!check.ok) return {
|
|
849
|
+
status: "denied",
|
|
850
|
+
message: check.message
|
|
851
|
+
};
|
|
852
|
+
let res;
|
|
853
|
+
try {
|
|
854
|
+
res = await this.fetchImpl(`${check.base}/api/access-grant`, {
|
|
855
|
+
method: "POST",
|
|
856
|
+
headers: {
|
|
857
|
+
authorization: `Bearer ${token}`,
|
|
858
|
+
"content-type": "application/json"
|
|
859
|
+
},
|
|
860
|
+
body: JSON.stringify({
|
|
861
|
+
workspaceSlug: ref.workspace,
|
|
862
|
+
projectSlug: ref.project
|
|
863
|
+
})
|
|
864
|
+
});
|
|
865
|
+
} catch (err) {
|
|
866
|
+
return {
|
|
867
|
+
status: "denied",
|
|
868
|
+
message: `Could not reach the access-grant endpoint: ${err?.message || err}`
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (400 === res.status) return {
|
|
872
|
+
status: "public"
|
|
873
|
+
};
|
|
874
|
+
if (!res.ok) return {
|
|
875
|
+
status: "denied",
|
|
876
|
+
httpStatus: res.status,
|
|
877
|
+
message: `access-grant returned ${res.status}`
|
|
878
|
+
};
|
|
879
|
+
let data;
|
|
880
|
+
try {
|
|
881
|
+
data = JSON.parse(await res.text());
|
|
882
|
+
} catch {
|
|
883
|
+
return {
|
|
884
|
+
status: "denied",
|
|
885
|
+
message: "access-grant did not return JSON"
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const minted = String(data?.token || "").trim();
|
|
889
|
+
if (!minted) return {
|
|
890
|
+
status: "denied",
|
|
891
|
+
message: "access-grant returned no token"
|
|
892
|
+
};
|
|
893
|
+
return {
|
|
894
|
+
status: "ok",
|
|
895
|
+
token: minted,
|
|
896
|
+
expiresAt: Number(data?.expiresAt || 0)
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
constructor(options){
|
|
900
|
+
_define_property(this, "cache", new Map());
|
|
901
|
+
_define_property(this, "fetchImpl", void 0);
|
|
902
|
+
_define_property(this, "nowSeconds", void 0);
|
|
903
|
+
this.fetchImpl = options?.fetchImpl ?? fetch;
|
|
904
|
+
this.nowSeconds = options?.nowSeconds ?? (()=>Math.floor(Date.now() / 1000));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const defaultRegistryAccessTokens = new RegistryAccessTokens();
|
|
908
|
+
function withAccessToken(url, token) {
|
|
909
|
+
if (!token) return url;
|
|
910
|
+
try {
|
|
911
|
+
const u = new URL(url);
|
|
912
|
+
u.searchParams.set("t", token);
|
|
913
|
+
return u.toString();
|
|
914
|
+
} catch {
|
|
915
|
+
return url;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const paths_0 = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/paths.cjs");
|
|
919
|
+
const userland = __webpack_require__("./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/userland.cjs");
|
|
517
920
|
origins.PROD_ORIGINS.registry;
|
|
518
921
|
function mcpOrigins(apiHint) {
|
|
519
922
|
const www = String(apiHint || process.env.EXTENSION_DEV_API_URL || "").trim() || void 0;
|
|
@@ -522,6 +925,7 @@ function mcpOrigins(apiHint) {
|
|
|
522
925
|
console: process.env.EXTENSION_DEV_CONSOLE_URL,
|
|
523
926
|
inspect: process.env.EXTENSION_DEV_INSPECT_URL,
|
|
524
927
|
preview: process.env.EXTENSION_DEV_PREVIEW_URL,
|
|
928
|
+
userland: process.env.EXTENSION_DEV_USERLAND_URL,
|
|
525
929
|
registry: process.env.EXTENSION_DEV_REGISTRY_URL,
|
|
526
930
|
media: process.env.EXTENSION_MEDIA_ORIGIN
|
|
527
931
|
}, {
|
|
@@ -558,33 +962,75 @@ function consoleProjectUrl(ref, page, apiHint) {
|
|
|
558
962
|
if (!ref) return base;
|
|
559
963
|
return `${base}${(0, paths_0.consoleProjectPath)(ref, page)}`;
|
|
560
964
|
}
|
|
561
|
-
|
|
965
|
+
function userlandProjectUrl(ref, page = "", apiHint) {
|
|
966
|
+
if (!ref) return "";
|
|
967
|
+
try {
|
|
968
|
+
return (0, userland.userlandUrl)(ref, page, {
|
|
969
|
+
base: mcpOrigins(apiHint).userland
|
|
970
|
+
});
|
|
971
|
+
} catch {
|
|
972
|
+
return "";
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function readJson(url, res) {
|
|
976
|
+
try {
|
|
977
|
+
const text = await res.text();
|
|
978
|
+
return {
|
|
979
|
+
ok: true,
|
|
980
|
+
json: JSON.parse(text)
|
|
981
|
+
};
|
|
982
|
+
} catch {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
message: `${url} did not return valid JSON`
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function fetchRegistryJson(url, fetchImpl = fetch, options) {
|
|
990
|
+
const tokens = options?.tokens ?? defaultRegistryAccessTokens;
|
|
991
|
+
const ref = options?.ref ?? null;
|
|
992
|
+
const cached = ref ? tokens.peek(ref) : "";
|
|
993
|
+
const firstUrl = cached ? withAccessToken(url, cached) : url;
|
|
562
994
|
let res;
|
|
563
995
|
try {
|
|
564
|
-
res = await fetchImpl(
|
|
996
|
+
res = await fetchImpl(firstUrl);
|
|
565
997
|
} catch (err) {
|
|
566
998
|
return {
|
|
567
999
|
ok: false,
|
|
568
1000
|
message: `Could not reach ${url}: ${err?.message || err}`
|
|
569
1001
|
};
|
|
570
1002
|
}
|
|
571
|
-
if (
|
|
1003
|
+
if (res.ok) return readJson(url, res);
|
|
1004
|
+
const authFailed = 401 === res.status || 403 === res.status;
|
|
1005
|
+
if (!authFailed || !ref) return {
|
|
572
1006
|
ok: false,
|
|
573
1007
|
status: res.status,
|
|
574
1008
|
message: `${url} returned ${res.status}`
|
|
575
1009
|
};
|
|
576
|
-
|
|
577
|
-
|
|
1010
|
+
const grant = await tokens.get(ref, options?.api);
|
|
1011
|
+
if ("ok" !== grant.status) {
|
|
1012
|
+
const detail = "no-credential" === grant.status ? "This project is private. Run extension_login for it, or set EXTENSION_DEV_TOKEN." : "public" === grant.status ? "The platform reports this project is public, but the registry refused the read." : grant.message;
|
|
578
1013
|
return {
|
|
579
|
-
ok:
|
|
580
|
-
|
|
1014
|
+
ok: false,
|
|
1015
|
+
status: res.status,
|
|
1016
|
+
message: `${url} returned ${res.status}. ${detail}`
|
|
581
1017
|
};
|
|
582
|
-
}
|
|
1018
|
+
}
|
|
1019
|
+
let retried;
|
|
1020
|
+
try {
|
|
1021
|
+
retried = await fetchImpl(withAccessToken(url, grant.token));
|
|
1022
|
+
} catch (err) {
|
|
583
1023
|
return {
|
|
584
1024
|
ok: false,
|
|
585
|
-
message:
|
|
1025
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
586
1026
|
};
|
|
587
1027
|
}
|
|
1028
|
+
if (!retried.ok) return {
|
|
1029
|
+
ok: false,
|
|
1030
|
+
status: retried.status,
|
|
1031
|
+
message: `${url} returned ${retried.status} even with an access token`
|
|
1032
|
+
};
|
|
1033
|
+
return readJson(url, retried);
|
|
588
1034
|
}
|
|
589
1035
|
function parseChannels(json) {
|
|
590
1036
|
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
@@ -3353,7 +3799,7 @@ function summarizeConsoleMessages(messages) {
|
|
|
3353
3799
|
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
3354
3800
|
};
|
|
3355
3801
|
}
|
|
3356
|
-
function
|
|
3802
|
+
function cdp_connection_define_property(obj, key, value) {
|
|
3357
3803
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
3358
3804
|
value: value,
|
|
3359
3805
|
enumerable: true,
|
|
@@ -3471,11 +3917,11 @@ class CDPConnection {
|
|
|
3471
3917
|
};
|
|
3472
3918
|
}
|
|
3473
3919
|
constructor(){
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3920
|
+
cdp_connection_define_property(this, "ws", null);
|
|
3921
|
+
cdp_connection_define_property(this, "messageId", 0);
|
|
3922
|
+
cdp_connection_define_property(this, "eventListeners", new Set());
|
|
3923
|
+
cdp_connection_define_property(this, "pendingRequests", new Map());
|
|
3924
|
+
cdp_connection_define_property(this, "consoleMessages", []);
|
|
3479
3925
|
}
|
|
3480
3926
|
}
|
|
3481
3927
|
const PAGE_HTML_SCRIPT = `(() => {
|
|
@@ -6205,110 +6651,6 @@ async function dom_inspect_handler(args) {
|
|
|
6205
6651
|
return raw;
|
|
6206
6652
|
}
|
|
6207
6653
|
}
|
|
6208
|
-
const DEFAULT_API = origins.PROD_ORIGINS.www;
|
|
6209
|
-
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
6210
|
-
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
6211
|
-
workspace: workspaceSlug,
|
|
6212
|
-
project: projectSlug
|
|
6213
|
-
}, "settings/access-tokens") : consoleBase();
|
|
6214
|
-
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}`;
|
|
6215
|
-
}
|
|
6216
|
-
function resolveApiBase(api) {
|
|
6217
|
-
return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
|
|
6218
|
-
}
|
|
6219
|
-
function safeApiBase(raw) {
|
|
6220
|
-
let parsed;
|
|
6221
|
-
try {
|
|
6222
|
-
parsed = new URL(raw);
|
|
6223
|
-
} catch {
|
|
6224
|
-
return {
|
|
6225
|
-
ok: false,
|
|
6226
|
-
message: `Invalid platform URL: ${raw}`
|
|
6227
|
-
};
|
|
6228
|
-
}
|
|
6229
|
-
const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
|
|
6230
|
-
if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
|
|
6231
|
-
ok: false,
|
|
6232
|
-
message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
|
|
6233
|
-
};
|
|
6234
|
-
return {
|
|
6235
|
-
ok: true,
|
|
6236
|
-
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
6237
|
-
};
|
|
6238
|
-
}
|
|
6239
|
-
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
6240
|
-
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
6241
|
-
if (override) return {
|
|
6242
|
-
provider: "github",
|
|
6243
|
-
clientId: override,
|
|
6244
|
-
scope: "read:user",
|
|
6245
|
-
deviceCodeUrl: "/api/cli/device/code",
|
|
6246
|
-
deviceTokenUrl: "/api/cli/device/token",
|
|
6247
|
-
verificationUri: "https://github.com/login/device"
|
|
6248
|
-
};
|
|
6249
|
-
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
6250
|
-
headers: {
|
|
6251
|
-
accept: "application/json"
|
|
6252
|
-
}
|
|
6253
|
-
});
|
|
6254
|
-
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
6255
|
-
const data = await res.json().catch(()=>({}));
|
|
6256
|
-
const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
|
|
6257
|
-
const clientId = String(data.githubClientId || "").trim();
|
|
6258
|
-
if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
6259
|
-
return {
|
|
6260
|
-
provider,
|
|
6261
|
-
clientId,
|
|
6262
|
-
scope: String(data.scope || "read:user"),
|
|
6263
|
-
deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
|
|
6264
|
-
deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
|
|
6265
|
-
verificationUri: String(data.verificationUri || "https://github.com/login/device")
|
|
6266
|
-
};
|
|
6267
|
-
}
|
|
6268
|
-
function persistTokenResponse(args) {
|
|
6269
|
-
const token = String(args.data.token || "").trim();
|
|
6270
|
-
if (!token) throw new Error("Login returned no token.");
|
|
6271
|
-
const creds = {
|
|
6272
|
-
version: 1,
|
|
6273
|
-
token,
|
|
6274
|
-
workspaceSlug: String(args.data.workspaceSlug || ""),
|
|
6275
|
-
projectSlug: String(args.data.projectSlug || ""),
|
|
6276
|
-
expiresAt: Number(args.data.expiresAt || 0),
|
|
6277
|
-
api: args.apiBase,
|
|
6278
|
-
provider: args.provider
|
|
6279
|
-
};
|
|
6280
|
-
writeCredentials(creds);
|
|
6281
|
-
return creds;
|
|
6282
|
-
}
|
|
6283
|
-
async function exchangeAndPersist(args) {
|
|
6284
|
-
const doFetch = args.fetchImpl ?? fetch;
|
|
6285
|
-
const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
|
|
6286
|
-
method: "POST",
|
|
6287
|
-
headers: {
|
|
6288
|
-
"content-type": "application/json",
|
|
6289
|
-
accept: "application/json"
|
|
6290
|
-
},
|
|
6291
|
-
body: JSON.stringify({
|
|
6292
|
-
githubToken: args.githubToken,
|
|
6293
|
-
project: args.project
|
|
6294
|
-
})
|
|
6295
|
-
});
|
|
6296
|
-
const text = await res.text();
|
|
6297
|
-
let data;
|
|
6298
|
-
try {
|
|
6299
|
-
data = JSON.parse(text);
|
|
6300
|
-
} catch {
|
|
6301
|
-
data = {
|
|
6302
|
-
message: text
|
|
6303
|
-
};
|
|
6304
|
-
}
|
|
6305
|
-
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
6306
|
-
return persistTokenResponse({
|
|
6307
|
-
apiBase: args.apiBase,
|
|
6308
|
-
data,
|
|
6309
|
-
provider: "github"
|
|
6310
|
-
});
|
|
6311
|
-
}
|
|
6312
6654
|
function resolveToken() {
|
|
6313
6655
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
6314
6656
|
if (fromEnv) return fromEnv;
|
|
@@ -6430,7 +6772,10 @@ async function publish_handler(args) {
|
|
|
6430
6772
|
const ref = resolveProjectRef();
|
|
6431
6773
|
if (ref) {
|
|
6432
6774
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
6433
|
-
const buildsRes = await fetchRegistryJson(buildsUrl
|
|
6775
|
+
const buildsRes = await fetchRegistryJson(buildsUrl, fetch, {
|
|
6776
|
+
ref,
|
|
6777
|
+
api: args.api
|
|
6778
|
+
});
|
|
6434
6779
|
if (buildsRes.ok) {
|
|
6435
6780
|
const items = parseBuildIndex(buildsRes.json);
|
|
6436
6781
|
const pinned = args.buildSha ? items.find((item)=>{
|
|
@@ -6552,7 +6897,10 @@ async function release_promote_handler(args) {
|
|
|
6552
6897
|
enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
|
|
6553
6898
|
if (ref) {
|
|
6554
6899
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
6555
|
-
const channelsRes = await fetchRegistryJson(channelsUrl
|
|
6900
|
+
const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
|
|
6901
|
+
ref,
|
|
6902
|
+
api: args.api
|
|
6903
|
+
});
|
|
6556
6904
|
if (channelsRes.ok) {
|
|
6557
6905
|
const rows = parseChannels(channelsRes.json).filter((c)=>c.sha);
|
|
6558
6906
|
enrich.validChannelShas = Object.fromEntries(rows.map((c)=>[
|
|
@@ -6575,11 +6923,23 @@ async function release_promote_handler(args) {
|
|
|
6575
6923
|
...enrich
|
|
6576
6924
|
});
|
|
6577
6925
|
}
|
|
6578
|
-
|
|
6926
|
+
const promotedRef = resolveProjectRef();
|
|
6927
|
+
const publicChannelUrl = userlandProjectUrl(promotedRef, userland.UserlandProjectPage.channel(channel), args.api);
|
|
6928
|
+
const publicBuildUrl = userlandProjectUrl(promotedRef, userland.UserlandProjectPage.build(buildId), args.api);
|
|
6929
|
+
const enriched = data && "object" == typeof data && !Array.isArray(data) ? {
|
|
6930
|
+
...data,
|
|
6931
|
+
...publicChannelUrl ? {
|
|
6932
|
+
publicChannelUrl
|
|
6933
|
+
} : {},
|
|
6934
|
+
...publicBuildUrl ? {
|
|
6935
|
+
publicBuildUrl
|
|
6936
|
+
} : {}
|
|
6937
|
+
} : data;
|
|
6938
|
+
return JSON.stringify(enriched);
|
|
6579
6939
|
}
|
|
6580
6940
|
const release_list_schema = {
|
|
6581
6941
|
name: "extension_release_list",
|
|
6582
|
-
description: "List the project's release channels (channel -> promoted build sha) and recent builds from the
|
|
6942
|
+
description: "List the project's release channels (channel -> promoted build sha) and recent builds from the registry (registry.extension.land), so you can pick a valid buildSha for extension_release_promote, extension_deploy, or extension_publish. Read-only, no dispatch. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another project. Works for PRIVATE projects too when your stored login covers them. Returns the registry URLs it read, the console Builds page URL (needs a login), and a publicUrl per channel and build on the public viewer (no login needed for a public project).",
|
|
6583
6943
|
inputSchema: {
|
|
6584
6944
|
type: "object",
|
|
6585
6945
|
properties: {
|
|
@@ -6590,6 +6950,10 @@ const release_list_schema = {
|
|
|
6590
6950
|
project: {
|
|
6591
6951
|
type: "string",
|
|
6592
6952
|
description: "Project slug override (defaults to the stored login's project)."
|
|
6953
|
+
},
|
|
6954
|
+
api: {
|
|
6955
|
+
type: "string",
|
|
6956
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
|
|
6593
6957
|
}
|
|
6594
6958
|
},
|
|
6595
6959
|
required: []
|
|
@@ -6612,12 +6976,21 @@ async function release_list_handler(args) {
|
|
|
6612
6976
|
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
6613
6977
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
6614
6978
|
const [channelsRes, metaRes, buildsRes] = await Promise.all([
|
|
6615
|
-
fetchRegistryJson(channelsUrl
|
|
6616
|
-
|
|
6617
|
-
|
|
6979
|
+
fetchRegistryJson(channelsUrl, fetch, {
|
|
6980
|
+
ref,
|
|
6981
|
+
api: args.api
|
|
6982
|
+
}),
|
|
6983
|
+
fetchRegistryJson(metaUrl, fetch, {
|
|
6984
|
+
ref,
|
|
6985
|
+
api: args.api
|
|
6986
|
+
}),
|
|
6987
|
+
fetchRegistryJson(buildsUrl, fetch, {
|
|
6988
|
+
ref,
|
|
6989
|
+
api: args.api
|
|
6990
|
+
})
|
|
6618
6991
|
]);
|
|
6619
|
-
const buildsPageUrl = consoleProjectUrl(ref, "builds");
|
|
6620
|
-
if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet,
|
|
6992
|
+
const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
6993
|
+
if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_login covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, {
|
|
6621
6994
|
workspace: ref.workspace,
|
|
6622
6995
|
project: ref.project,
|
|
6623
6996
|
registryUrl: channelsUrl,
|
|
@@ -6628,6 +7001,16 @@ async function release_list_handler(args) {
|
|
|
6628
7001
|
recentBuilds.sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
|
|
6629
7002
|
const meta = metaRes.ok ? metaRes.json : void 0;
|
|
6630
7003
|
const promotable = Array.from(new Set(channels.map((c)=>c.sha).filter(Boolean)));
|
|
7004
|
+
const isPrivate = "private" === String(meta?.visibility || "").toLowerCase();
|
|
7005
|
+
const publicProjectUrl = userlandProjectUrl(ref, "", args.api);
|
|
7006
|
+
const channelsWithUrls = channels.map((c)=>({
|
|
7007
|
+
...c,
|
|
7008
|
+
publicUrl: userlandProjectUrl(ref, userland.UserlandProjectPage.channel(c.channel), args.api)
|
|
7009
|
+
}));
|
|
7010
|
+
const buildsWithUrls = recentBuilds.map((b)=>({
|
|
7011
|
+
...b,
|
|
7012
|
+
publicUrl: userlandProjectUrl(ref, userland.UserlandProjectPage.build(b.sha), args.api)
|
|
7013
|
+
}));
|
|
6631
7014
|
const result = {
|
|
6632
7015
|
ok: true,
|
|
6633
7016
|
workspace: ref.workspace,
|
|
@@ -6638,10 +7021,16 @@ async function release_list_handler(args) {
|
|
|
6638
7021
|
...meta?.visibility ? {
|
|
6639
7022
|
visibility: meta.visibility
|
|
6640
7023
|
} : {},
|
|
6641
|
-
channels,
|
|
6642
|
-
recentBuilds,
|
|
7024
|
+
channels: channelsWithUrls,
|
|
7025
|
+
recentBuilds: buildsWithUrls,
|
|
6643
7026
|
registryUrl: channelsUrl,
|
|
6644
7027
|
buildsPageUrl,
|
|
7028
|
+
...publicProjectUrl ? {
|
|
7029
|
+
publicProjectUrl
|
|
7030
|
+
} : {},
|
|
7031
|
+
...publicProjectUrl ? {
|
|
7032
|
+
publicUrlNote: isPrivate ? "publicUrl links open only for workspace members. This project is private, so an outside recipient needs a share link from extension_publish." : "publicUrl links are the public build pages: no login needed, and they carry the per-browser downloads and the run locally instructions."
|
|
7033
|
+
} : {},
|
|
6645
7034
|
message: promotable.length > 0 || recentBuilds.length > 0 ? `Promotable shas: channels currently pin ${promotable.length > 0 ? promotable.join(", ") : "none"}; recent builds add ${recentBuilds.filter((b)=>"success" === b.status).map((b)=>b.sha).join(", ") || "none"}. Use one of these as buildId/buildSha for promote/deploy/publish.` : `No channels or builds are recorded on the registry yet for ${ref.workspace}/${ref.project}. Push a commit to produce a build, then check ${buildsPageUrl}.`
|
|
6646
7035
|
};
|
|
6647
7036
|
if (!channelsRes.ok) result.channelsUnavailable = `channels.json unreadable: ${channelsRes.message}`;
|
|
@@ -6789,8 +7178,14 @@ async function deploy_handler(args) {
|
|
|
6789
7178
|
let channelRows = null;
|
|
6790
7179
|
if (ref) {
|
|
6791
7180
|
const [healthRes, channelsRes] = await Promise.all([
|
|
6792
|
-
fetchRegistryJson(registryFileUrl(ref, "stores/health.json")
|
|
6793
|
-
|
|
7181
|
+
fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
|
|
7182
|
+
ref,
|
|
7183
|
+
api: args.api
|
|
7184
|
+
}),
|
|
7185
|
+
fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
|
|
7186
|
+
ref,
|
|
7187
|
+
api: args.api
|
|
7188
|
+
})
|
|
6794
7189
|
]);
|
|
6795
7190
|
if (healthRes.ok) {
|
|
6796
7191
|
const stores = healthRes.json?.stores;
|
|
@@ -6873,6 +7268,10 @@ const store_status_schema = {
|
|
|
6873
7268
|
project: {
|
|
6874
7269
|
type: "string",
|
|
6875
7270
|
description: "Project slug override (defaults to the stored login's project)."
|
|
7271
|
+
},
|
|
7272
|
+
api: {
|
|
7273
|
+
type: "string",
|
|
7274
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
|
|
6876
7275
|
}
|
|
6877
7276
|
},
|
|
6878
7277
|
required: []
|
|
@@ -6972,11 +7371,20 @@ async function store_status_handler(args) {
|
|
|
6972
7371
|
const healthUrl = registryFileUrl(ref, "stores/health.json");
|
|
6973
7372
|
const statusUrl = registryFileUrl(ref, "stores/status.json");
|
|
6974
7373
|
const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
|
|
6975
|
-
const consoleStoresUrl = consoleProjectUrl(ref, "stores");
|
|
7374
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
6976
7375
|
const [healthRes, statusRes, submissionsRes] = await Promise.all([
|
|
6977
|
-
fetchRegistryJson(healthUrl
|
|
6978
|
-
|
|
6979
|
-
|
|
7376
|
+
fetchRegistryJson(healthUrl, fetch, {
|
|
7377
|
+
ref,
|
|
7378
|
+
api: args.api
|
|
7379
|
+
}),
|
|
7380
|
+
fetchRegistryJson(statusUrl, fetch, {
|
|
7381
|
+
ref,
|
|
7382
|
+
api: args.api
|
|
7383
|
+
}),
|
|
7384
|
+
fetchRegistryJson(submissionsUrl, fetch, {
|
|
7385
|
+
ref,
|
|
7386
|
+
api: args.api
|
|
7387
|
+
})
|
|
6980
7388
|
]);
|
|
6981
7389
|
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}`, {
|
|
6982
7390
|
workspace: ref.workspace,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ProjectRef } from "./registry";
|
|
2
|
+
export type AccessGrant = {
|
|
3
|
+
status: "ok";
|
|
4
|
+
token: string;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
}
|
|
7
|
+
/** Project is public; no token is needed and none was minted. */
|
|
8
|
+
| {
|
|
9
|
+
status: "public";
|
|
10
|
+
}
|
|
11
|
+
/** No stored credential, or it does not cover the requested project. */
|
|
12
|
+
| {
|
|
13
|
+
status: "no-credential";
|
|
14
|
+
}
|
|
15
|
+
/** Reached the platform and it refused (401/403/5xx, network, bad body). */
|
|
16
|
+
| {
|
|
17
|
+
status: "denied";
|
|
18
|
+
httpStatus?: number;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
export interface AccessTokenCacheOptions {
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
/** Test-only clock override, unix epoch seconds. */
|
|
24
|
+
nowSeconds?: () => number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Per-process cache of minted tokens. Concurrent readers of the same project
|
|
28
|
+
* share one in-flight mint: a single tool call fans out over meta.json,
|
|
29
|
+
* channels.json and builds/index.json, and three cold 401s must not become
|
|
30
|
+
* three mint round-trips.
|
|
31
|
+
*/
|
|
32
|
+
export declare class RegistryAccessTokens {
|
|
33
|
+
private readonly cache;
|
|
34
|
+
private readonly fetchImpl;
|
|
35
|
+
private readonly nowSeconds;
|
|
36
|
+
constructor(options?: AccessTokenCacheOptions);
|
|
37
|
+
/** A cached, still-valid token, or "" if none. Never mints. */
|
|
38
|
+
peek(ref: ProjectRef): string;
|
|
39
|
+
get(ref: ProjectRef, apiHint?: string): Promise<AccessGrant>;
|
|
40
|
+
private mint;
|
|
41
|
+
}
|
|
42
|
+
/** Process-wide default, so one tool call's fan-out shares one mint. */
|
|
43
|
+
export declare const defaultRegistryAccessTokens: RegistryAccessTokens;
|
|
44
|
+
/** Append `?t=<token>` to a registry URL. */
|
|
45
|
+
export declare function withAccessToken(url: string, token: string): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RegistryAccessTokens } from "./registry-access";
|
|
1
2
|
import { type Origins } from "@extension.dev/urls/origins";
|
|
2
3
|
export declare const REGISTRY_BASE_DEFAULT: string;
|
|
3
4
|
/**
|
|
@@ -37,6 +38,20 @@ export declare function registryFileUrl(ref: ProjectRef, file: string): string;
|
|
|
37
38
|
* `EXTENSION_DEV_API_URL`/`EXTENSION_DEV_CONSOLE_URL`.
|
|
38
39
|
*/
|
|
39
40
|
export declare function consoleProjectUrl(ref: ProjectRef | null, page: string, apiHint?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* The PUBLIC page for a project, a build, or a channel on userland.
|
|
43
|
+
*
|
|
44
|
+
* The sibling of consoleProjectUrl, and the one to hand to a human who is not
|
|
45
|
+
* the operator: the console links every tool already returns require a login
|
|
46
|
+
* and workspace membership, so they are dead ends for a teammate, a reviewer,
|
|
47
|
+
* or anyone reading a PR. These carry the per-browser downloads, the run
|
|
48
|
+
* locally and integrity dialogs, and the what's new tab.
|
|
49
|
+
*
|
|
50
|
+
* A PRIVATE project's page still needs a `?share=` token, which only
|
|
51
|
+
* extension_publish can mint; callers should say so rather than imply the bare
|
|
52
|
+
* URL will open for anyone.
|
|
53
|
+
*/
|
|
54
|
+
export declare function userlandProjectUrl(ref: ProjectRef | null, page?: string, apiHint?: string): string;
|
|
40
55
|
export type RegistryFetchResult<T> = {
|
|
41
56
|
ok: true;
|
|
42
57
|
json: T;
|
|
@@ -46,11 +61,23 @@ export type RegistryFetchResult<T> = {
|
|
|
46
61
|
message: string;
|
|
47
62
|
};
|
|
48
63
|
/**
|
|
49
|
-
* Fetch a registry JSON file. Never throws: 404s (
|
|
50
|
-
*
|
|
51
|
-
*
|
|
64
|
+
* Fetch a registry JSON file. Never throws: 404s (never-built projects),
|
|
65
|
+
* network failures, and non-JSON bodies all come back as `{ok:false}` so
|
|
66
|
+
* callers can degrade honestly instead of crashing the verb.
|
|
67
|
+
*
|
|
68
|
+
* PRIVATE PROJECTS: a private project answers 401 here. When `ref` is given and
|
|
69
|
+
* the caller holds a credential for that project, the 401 triggers one mint of
|
|
70
|
+
* a short-lived access token and a single retry with `?t=`. Without `ref` the
|
|
71
|
+
* behaviour is exactly as before, so this stays a drop-in for public reads:
|
|
72
|
+
* public projects still complete in one round trip and never touch the
|
|
73
|
+
* platform. See registry-access.ts for why the stored token is traded rather
|
|
74
|
+
* than sent as `?t=` directly.
|
|
52
75
|
*/
|
|
53
|
-
export declare function fetchRegistryJson<T = unknown>(url: string, fetchImpl?: typeof fetch
|
|
76
|
+
export declare function fetchRegistryJson<T = unknown>(url: string, fetchImpl?: typeof fetch, options?: {
|
|
77
|
+
ref?: ProjectRef | null;
|
|
78
|
+
api?: string;
|
|
79
|
+
tokens?: RegistryAccessTokens;
|
|
80
|
+
}): Promise<RegistryFetchResult<T>>;
|
|
54
81
|
export interface ChannelEntry {
|
|
55
82
|
channel: string;
|
|
56
83
|
sha: string;
|
|
@@ -12,6 +12,10 @@ export declare const schema: {
|
|
|
12
12
|
type: string;
|
|
13
13
|
description: string;
|
|
14
14
|
};
|
|
15
|
+
api: {
|
|
16
|
+
type: string;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
15
19
|
};
|
|
16
20
|
required: never[];
|
|
17
21
|
};
|
|
@@ -19,4 +23,5 @@ export declare const schema: {
|
|
|
19
23
|
export declare function handler(args: {
|
|
20
24
|
workspace?: string;
|
|
21
25
|
project?: string;
|
|
26
|
+
api?: string;
|
|
22
27
|
}): Promise<string>;
|
|
@@ -12,6 +12,10 @@ export declare const schema: {
|
|
|
12
12
|
type: string;
|
|
13
13
|
description: string;
|
|
14
14
|
};
|
|
15
|
+
api: {
|
|
16
|
+
type: string;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
15
19
|
};
|
|
16
20
|
required: never[];
|
|
17
21
|
};
|
|
@@ -52,5 +56,6 @@ export declare function normalizeStoresStatus(json: unknown): NormalizedStoresSt
|
|
|
52
56
|
export declare function handler(args: {
|
|
53
57
|
workspace?: string;
|
|
54
58
|
project?: string;
|
|
59
|
+
api?: string;
|
|
55
60
|
}): Promise<string>;
|
|
56
61
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.
|
|
4
|
+
"version": "6.3.0",
|
|
5
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": "Apache-2.0",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"ws": "^8.20.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
|
-
"@extension.dev/urls": "^0.
|
|
100
|
+
"@extension.dev/urls": "^0.3.0",
|
|
101
101
|
"@rslib/core": "^0.19.4",
|
|
102
102
|
"@types/cross-spawn": "^6.0.6",
|
|
103
103
|
"@types/node": "^25.2.0",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "6.
|
|
10
|
+
"version": "6.3.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.3.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|