@extension.dev/mcp 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9 -0
- package/README.md +1 -2
- package/dist/module.js +295 -8
- package/dist/src/lib/credentials.d.ts +24 -0
- package/dist/src/lib/github-device.d.ts +40 -0
- package/dist/src/lib/login-flow.d.ts +40 -0
- package/dist/src/lib/publish.d.ts +31 -0
- package/package.json +1 -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": "4.
|
|
13
|
+
"version": "4.5.0",
|
|
14
14
|
"category": "development",
|
|
15
15
|
"author": {
|
|
16
16
|
"name": "Cezar Augusto"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extension-mcp",
|
|
3
3
|
"description": "MCP tools for browser extension development: scaffold from 60+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox. Ships /extension, /extension-add, /extension-debug, and /extension-publish commands.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.5.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "boss@cezaraugusto.net",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.0
|
|
4
|
+
|
|
5
|
+
The platform client (GitHub device-code login, the credential store, and the
|
|
6
|
+
publish flow) is now vendored directly in this package instead of the
|
|
7
|
+
separate `@extension.dev/core` dependency. No behavior change: the tool
|
|
8
|
+
schemas, the credential file, token resolution, and the publish error
|
|
9
|
+
envelopes are all unchanged. This drops a runtime dependency and the
|
|
10
|
+
two-package release step.
|
|
11
|
+
|
|
3
12
|
## 4.4.0
|
|
4
13
|
|
|
5
14
|
Browser-matrix parity release: the tool surface now mirrors the engine
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
> Give your AI agent hands for browser extension development. 30 MCP tools that scaffold, run, inspect, debug, and publish cross-browser extensions.
|
|
11
11
|
|
|
12
|
-
<img alt="Logo" align="right" src="https://media.extension.land/brand/extension-dev/logo-dock.png" width="
|
|
12
|
+
<img alt="Logo" align="right" src="https://media.extension.land/brand/extension-dev/logo-dock.png" width="15.5%" />
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
claude mcp add extension-dev npx @extension.dev/mcp
|
|
@@ -134,7 +134,6 @@ The four platform tools connect agents to [extension.dev](https://extension.dev)
|
|
|
134
134
|
| --- | --- |
|
|
135
135
|
| [`@extension.dev/skill`](https://www.npmjs.com/package/@extension.dev/skill) | Teach AI agents the judgment half: cross-browser rules, gotchas, playbooks |
|
|
136
136
|
| [`@extension.dev/deploy`](https://www.npmjs.com/package/@extension.dev/deploy) | Ship to Chrome, Firefox, and Edge stores from CI |
|
|
137
|
-
| [`@extension.dev/core`](https://www.npmjs.com/package/@extension.dev/core) | Authenticate and publish to the extension.dev platform |
|
|
138
137
|
|
|
139
138
|
All of it rides on [Extension.js](https://github.com/extension-js/extension.js), the open-source cross-browser extension framework.
|
|
140
139
|
|
package/dist/module.js
CHANGED
|
@@ -11,7 +11,6 @@ import cross_spawn from "cross-spawn";
|
|
|
11
11
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
12
12
|
import ws from "ws";
|
|
13
13
|
import node_http from "node:http";
|
|
14
|
-
import { clearCredentials, exchangeAndPersist, fetchLoginConfig, pollForToken, publish, readCredentials, resolveApiBase, resolveToken, safeApiBase, startDeviceCode } from "@extension.dev/core";
|
|
15
14
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
16
15
|
import { execFile } from "node:child_process";
|
|
17
16
|
import { promisify } from "node:util";
|
|
@@ -135,9 +134,9 @@ __webpack_require__.d(preview_namespaceObject, {
|
|
|
135
134
|
handler: ()=>preview_handler,
|
|
136
135
|
schema: ()=>preview_schema
|
|
137
136
|
});
|
|
138
|
-
var
|
|
139
|
-
__webpack_require__.r(
|
|
140
|
-
__webpack_require__.d(
|
|
137
|
+
var tools_publish_namespaceObject = {};
|
|
138
|
+
__webpack_require__.r(tools_publish_namespaceObject);
|
|
139
|
+
__webpack_require__.d(tools_publish_namespaceObject, {
|
|
141
140
|
handler: ()=>publish_handler,
|
|
142
141
|
schema: ()=>publish_schema
|
|
143
142
|
});
|
|
@@ -195,7 +194,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
195
194
|
handler: ()=>whoami_handler,
|
|
196
195
|
schema: ()=>whoami_schema
|
|
197
196
|
});
|
|
198
|
-
var package_namespaceObject = JSON.parse('{"rE":"4.
|
|
197
|
+
var package_namespaceObject = JSON.parse('{"rE":"4.5.0","El":{"OP":"^4.0.11"}}');
|
|
199
198
|
const create_schema = {
|
|
200
199
|
name: "extension_create",
|
|
201
200
|
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
@@ -2790,6 +2789,222 @@ async function dom_inspect_handler(args) {
|
|
|
2790
2789
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2791
2790
|
return runActVerb(cli, args.projectPath, args.timeout);
|
|
2792
2791
|
}
|
|
2792
|
+
function credentialsPath() {
|
|
2793
|
+
if ("win32" === process.platform) {
|
|
2794
|
+
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
2795
|
+
return node_path.join(base, "extension-dev", "auth.json");
|
|
2796
|
+
}
|
|
2797
|
+
const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
|
|
2798
|
+
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
2799
|
+
return node_path.join(base, "extension-dev", "auth.json");
|
|
2800
|
+
}
|
|
2801
|
+
function readCredentials() {
|
|
2802
|
+
try {
|
|
2803
|
+
const raw = node_fs.readFileSync(credentialsPath(), "utf8");
|
|
2804
|
+
const data = JSON.parse(raw);
|
|
2805
|
+
if (!data || "object" != typeof data) return null;
|
|
2806
|
+
if (1 !== data.version) return null;
|
|
2807
|
+
const token = String(data.token || "").trim();
|
|
2808
|
+
if (!token) return null;
|
|
2809
|
+
return {
|
|
2810
|
+
version: 1,
|
|
2811
|
+
token,
|
|
2812
|
+
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2813
|
+
projectSlug: String(data.projectSlug || ""),
|
|
2814
|
+
expiresAt: Number(data.expiresAt || 0),
|
|
2815
|
+
api: String(data.api || "")
|
|
2816
|
+
};
|
|
2817
|
+
} catch {
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
function writeCredentials(creds) {
|
|
2822
|
+
const file = credentialsPath();
|
|
2823
|
+
const dir = node_path.dirname(file);
|
|
2824
|
+
node_fs.mkdirSync(dir, {
|
|
2825
|
+
recursive: true,
|
|
2826
|
+
mode: 448
|
|
2827
|
+
});
|
|
2828
|
+
try {
|
|
2829
|
+
node_fs.chmodSync(dir, 448);
|
|
2830
|
+
} catch {}
|
|
2831
|
+
node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
|
|
2832
|
+
mode: 384
|
|
2833
|
+
});
|
|
2834
|
+
try {
|
|
2835
|
+
node_fs.chmodSync(file, 384);
|
|
2836
|
+
} catch {}
|
|
2837
|
+
return file;
|
|
2838
|
+
}
|
|
2839
|
+
function clearCredentials() {
|
|
2840
|
+
const file = credentialsPath();
|
|
2841
|
+
try {
|
|
2842
|
+
node_fs.unlinkSync(file);
|
|
2843
|
+
return {
|
|
2844
|
+
cleared: true,
|
|
2845
|
+
path: file
|
|
2846
|
+
};
|
|
2847
|
+
} catch {
|
|
2848
|
+
return {
|
|
2849
|
+
cleared: false,
|
|
2850
|
+
path: file
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
2855
|
+
const creds = readCredentials();
|
|
2856
|
+
if (!creds) return null;
|
|
2857
|
+
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
2858
|
+
return creds;
|
|
2859
|
+
}
|
|
2860
|
+
const DEFAULT_API = "https://www.extension.dev";
|
|
2861
|
+
function resolveApiBase(api) {
|
|
2862
|
+
return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
|
|
2863
|
+
}
|
|
2864
|
+
function safeApiBase(raw) {
|
|
2865
|
+
let parsed;
|
|
2866
|
+
try {
|
|
2867
|
+
parsed = new URL(raw);
|
|
2868
|
+
} catch {
|
|
2869
|
+
return {
|
|
2870
|
+
ok: false,
|
|
2871
|
+
message: `Invalid platform URL: ${raw}`
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
|
|
2875
|
+
if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
|
|
2876
|
+
ok: false,
|
|
2877
|
+
message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
|
|
2878
|
+
};
|
|
2879
|
+
return {
|
|
2880
|
+
ok: true,
|
|
2881
|
+
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
2885
|
+
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
2886
|
+
if (override) return {
|
|
2887
|
+
clientId: override,
|
|
2888
|
+
scope: "read:user"
|
|
2889
|
+
};
|
|
2890
|
+
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
2891
|
+
headers: {
|
|
2892
|
+
accept: "application/json"
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
2896
|
+
const data = await res.json().catch(()=>({}));
|
|
2897
|
+
const clientId = String(data.githubClientId || "").trim();
|
|
2898
|
+
if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
2899
|
+
return {
|
|
2900
|
+
clientId,
|
|
2901
|
+
scope: String(data.scope || "read:user")
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
async function exchangeAndPersist(args) {
|
|
2905
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
2906
|
+
const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
|
|
2907
|
+
method: "POST",
|
|
2908
|
+
headers: {
|
|
2909
|
+
"content-type": "application/json",
|
|
2910
|
+
accept: "application/json"
|
|
2911
|
+
},
|
|
2912
|
+
body: JSON.stringify({
|
|
2913
|
+
githubToken: args.githubToken,
|
|
2914
|
+
project: args.project
|
|
2915
|
+
})
|
|
2916
|
+
});
|
|
2917
|
+
const text = await res.text();
|
|
2918
|
+
let data;
|
|
2919
|
+
try {
|
|
2920
|
+
data = JSON.parse(text);
|
|
2921
|
+
} catch {
|
|
2922
|
+
data = {
|
|
2923
|
+
message: text
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
2927
|
+
const token = String(data.token || "").trim();
|
|
2928
|
+
if (!token) throw new Error("Login exchange returned no token.");
|
|
2929
|
+
const creds = {
|
|
2930
|
+
version: 1,
|
|
2931
|
+
token,
|
|
2932
|
+
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2933
|
+
projectSlug: String(data.projectSlug || ""),
|
|
2934
|
+
expiresAt: Number(data.expiresAt || 0),
|
|
2935
|
+
api: args.apiBase
|
|
2936
|
+
};
|
|
2937
|
+
writeCredentials(creds);
|
|
2938
|
+
return creds;
|
|
2939
|
+
}
|
|
2940
|
+
function resolveToken() {
|
|
2941
|
+
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
2942
|
+
if (fromEnv) return fromEnv;
|
|
2943
|
+
const creds = readValidCredentials();
|
|
2944
|
+
return creds?.token ? String(creds.token).trim() : "";
|
|
2945
|
+
}
|
|
2946
|
+
async function publish(options = {}) {
|
|
2947
|
+
const token = options.token ?? resolveToken();
|
|
2948
|
+
if (!token) return {
|
|
2949
|
+
ok: false,
|
|
2950
|
+
error: {
|
|
2951
|
+
name: "PublishAuthError",
|
|
2952
|
+
message: "No token. Run login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
2956
|
+
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
2957
|
+
if (!apiCheck.ok) return {
|
|
2958
|
+
ok: false,
|
|
2959
|
+
error: {
|
|
2960
|
+
name: "PublishConfigError",
|
|
2961
|
+
message: apiCheck.message
|
|
2962
|
+
}
|
|
2963
|
+
};
|
|
2964
|
+
const url = `${apiCheck.base}/api/cli/publish`;
|
|
2965
|
+
const body = {};
|
|
2966
|
+
if (null != options.ttlHours) body.ttlHours = Number(options.ttlHours);
|
|
2967
|
+
if (options.buildSha) body.buildSha = options.buildSha;
|
|
2968
|
+
let res;
|
|
2969
|
+
try {
|
|
2970
|
+
res = await doFetch(url, {
|
|
2971
|
+
method: "POST",
|
|
2972
|
+
headers: {
|
|
2973
|
+
authorization: `Bearer ${token}`,
|
|
2974
|
+
"content-type": "application/json"
|
|
2975
|
+
},
|
|
2976
|
+
body: JSON.stringify(body)
|
|
2977
|
+
});
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
return {
|
|
2980
|
+
ok: false,
|
|
2981
|
+
error: {
|
|
2982
|
+
name: "PublishNetworkError",
|
|
2983
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
const text = await res.text();
|
|
2988
|
+
let data;
|
|
2989
|
+
try {
|
|
2990
|
+
data = JSON.parse(text);
|
|
2991
|
+
} catch {
|
|
2992
|
+
data = {
|
|
2993
|
+
message: text
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
if (!res.ok) return {
|
|
2997
|
+
ok: false,
|
|
2998
|
+
error: {
|
|
2999
|
+
name: "PublishError",
|
|
3000
|
+
message: `publish failed (${res.status}): ${data?.message || text || "unknown error"}`
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
return {
|
|
3004
|
+
ok: true,
|
|
3005
|
+
data
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
2793
3008
|
const publish_schema = {
|
|
2794
3009
|
name: "extension_publish",
|
|
2795
3010
|
description: "Publish a project to extension.dev and return a shareable URL. Auth-gated: requires EXTENSION_DEV_TOKEN (a workspace/project access token) in the environment. Posts to the platform's CLI publish endpoint directly. This is the only tool that talks to the hosted platform rather than the local browser.",
|
|
@@ -2835,7 +3050,7 @@ async function publish_handler(args) {
|
|
|
2835
3050
|
});
|
|
2836
3051
|
return result.ok ? JSON.stringify(result.data) : JSON.stringify(result);
|
|
2837
3052
|
}
|
|
2838
|
-
const
|
|
3053
|
+
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
2839
3054
|
const release_promote_schema = {
|
|
2840
3055
|
name: "extension_release_promote",
|
|
2841
3056
|
description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: requires a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
|
|
@@ -2895,7 +3110,7 @@ async function release_promote_handler(args) {
|
|
|
2895
3110
|
const buildId = String(args.buildId || "").trim();
|
|
2896
3111
|
const channel = String(args.channel || "").trim();
|
|
2897
3112
|
if (!buildId || !channel) return fail("ReleaseInputError", "buildId and channel are required.");
|
|
2898
|
-
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL ||
|
|
3113
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
|
|
2899
3114
|
if (!apiCheck.ok) return fail("ReleaseConfigError", apiCheck.message);
|
|
2900
3115
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
2901
3116
|
const body = {
|
|
@@ -3225,6 +3440,78 @@ async function add_feature_handler(args) {
|
|
|
3225
3440
|
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
3226
3441
|
});
|
|
3227
3442
|
}
|
|
3443
|
+
const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
3444
|
+
const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
3445
|
+
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
3446
|
+
const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
3447
|
+
async function startDeviceCode(args) {
|
|
3448
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
3449
|
+
const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
|
|
3450
|
+
method: "POST",
|
|
3451
|
+
headers: {
|
|
3452
|
+
accept: "application/json",
|
|
3453
|
+
"content-type": "application/json"
|
|
3454
|
+
},
|
|
3455
|
+
body: JSON.stringify({
|
|
3456
|
+
client_id: args.clientId,
|
|
3457
|
+
scope: args.scope || "read:user"
|
|
3458
|
+
})
|
|
3459
|
+
});
|
|
3460
|
+
const data = await res.json().catch(()=>({}));
|
|
3461
|
+
if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
|
|
3462
|
+
return {
|
|
3463
|
+
deviceCode: String(data.device_code || ""),
|
|
3464
|
+
userCode: String(data.user_code || ""),
|
|
3465
|
+
verificationUri: String(data.verification_uri || "https://github.com/login/device"),
|
|
3466
|
+
interval: Number(data.interval || 5),
|
|
3467
|
+
expiresIn: Number(data.expires_in || 900)
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
async function pollForToken(args) {
|
|
3471
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
3472
|
+
const sleep = args.sleepImpl ?? defaultSleep;
|
|
3473
|
+
let intervalMs = 1000 * Math.max(1, args.interval);
|
|
3474
|
+
const deadline = Date.now() + Math.max(0, args.budgetMs);
|
|
3475
|
+
while(Date.now() < deadline){
|
|
3476
|
+
await sleep(intervalMs);
|
|
3477
|
+
const res = await doFetch(GITHUB_TOKEN_URL, {
|
|
3478
|
+
method: "POST",
|
|
3479
|
+
headers: {
|
|
3480
|
+
accept: "application/json",
|
|
3481
|
+
"content-type": "application/json"
|
|
3482
|
+
},
|
|
3483
|
+
body: JSON.stringify({
|
|
3484
|
+
client_id: args.clientId,
|
|
3485
|
+
device_code: args.deviceCode,
|
|
3486
|
+
grant_type: DEVICE_GRANT_TYPE
|
|
3487
|
+
})
|
|
3488
|
+
});
|
|
3489
|
+
const data = await res.json().catch(()=>({}));
|
|
3490
|
+
const token = String(data.access_token || "").trim();
|
|
3491
|
+
if (token) return {
|
|
3492
|
+
ok: true,
|
|
3493
|
+
githubToken: token
|
|
3494
|
+
};
|
|
3495
|
+
const error = String(data.error || "");
|
|
3496
|
+
if ("authorization_pending" === error) continue;
|
|
3497
|
+
if ("slow_down" === error) {
|
|
3498
|
+
intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
|
|
3499
|
+
continue;
|
|
3500
|
+
}
|
|
3501
|
+
if ("expired_token" === error) return {
|
|
3502
|
+
ok: false,
|
|
3503
|
+
reason: "expired"
|
|
3504
|
+
};
|
|
3505
|
+
if ("access_denied" === error) return {
|
|
3506
|
+
ok: false,
|
|
3507
|
+
reason: "denied"
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
return {
|
|
3511
|
+
ok: false,
|
|
3512
|
+
reason: "pending"
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3228
3515
|
const login_schema = {
|
|
3229
3516
|
name: "extension_login",
|
|
3230
3517
|
description: "Authenticate to extension.dev via GitHub device-code and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
@@ -4006,7 +4293,7 @@ const tools = [
|
|
|
4006
4293
|
reload_namespaceObject,
|
|
4007
4294
|
open_namespaceObject,
|
|
4008
4295
|
dom_inspect_namespaceObject,
|
|
4009
|
-
|
|
4296
|
+
tools_publish_namespaceObject,
|
|
4010
4297
|
release_promote_namespaceObject,
|
|
4011
4298
|
wait_namespaceObject,
|
|
4012
4299
|
add_feature_namespaceObject,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface StoredCredentials {
|
|
2
|
+
version: 1;
|
|
3
|
+
/** The HMAC access token the publish path sends as `Bearer`. */
|
|
4
|
+
token: string;
|
|
5
|
+
workspaceSlug: string;
|
|
6
|
+
projectSlug: string;
|
|
7
|
+
/** Token expiry as unix epoch seconds (0 if unknown). */
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
/** Platform base URL the token was minted against. */
|
|
10
|
+
api: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function credentialsPath(): string;
|
|
13
|
+
export declare function readCredentials(): StoredCredentials | null;
|
|
14
|
+
export declare function writeCredentials(creds: StoredCredentials): string;
|
|
15
|
+
export declare function clearCredentials(): {
|
|
16
|
+
cleared: boolean;
|
|
17
|
+
path: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Return stored credentials only if the token has not expired. Used by the
|
|
21
|
+
* publish token resolution so an expired local token falls through cleanly to
|
|
22
|
+
* "no token" instead of producing a 401 from the platform.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readValidCredentials(nowSeconds?: number): StoredCredentials | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface DeviceCodeStart {
|
|
2
|
+
deviceCode: string;
|
|
3
|
+
userCode: string;
|
|
4
|
+
verificationUri: string;
|
|
5
|
+
/** Minimum seconds to wait between polls. */
|
|
6
|
+
interval: number;
|
|
7
|
+
/** Seconds until the device code expires. */
|
|
8
|
+
expiresIn: number;
|
|
9
|
+
}
|
|
10
|
+
export type PollResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
githubToken: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: "pending" | "expired" | "denied";
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
type FetchImpl = typeof fetch;
|
|
19
|
+
type SleepImpl = (ms: number) => Promise<void>;
|
|
20
|
+
export declare function startDeviceCode(args: {
|
|
21
|
+
clientId: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
fetchImpl?: FetchImpl;
|
|
24
|
+
}): Promise<DeviceCodeStart>;
|
|
25
|
+
/**
|
|
26
|
+
* Poll for the access token until the budget elapses. A budget shorter than
|
|
27
|
+
* the device-code lifetime lets a caller (e.g. an MCP tool that must return
|
|
28
|
+
* promptly) poll in bounded slices and report "pending" between calls; a long
|
|
29
|
+
* budget (a blocking CLI) waits the whole time. Returns `pending` only on
|
|
30
|
+
* budget exhaustion; `expired`/`denied` are terminal.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pollForToken(args: {
|
|
33
|
+
clientId: string;
|
|
34
|
+
deviceCode: string;
|
|
35
|
+
interval: number;
|
|
36
|
+
budgetMs: number;
|
|
37
|
+
fetchImpl?: FetchImpl;
|
|
38
|
+
sleepImpl?: SleepImpl;
|
|
39
|
+
}): Promise<PollResult>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type StoredCredentials } from "./credentials";
|
|
2
|
+
type FetchImpl = typeof fetch;
|
|
3
|
+
export declare function resolveApiBase(api?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Validate the platform base URL BEFORE we attach a bearer token to a request to
|
|
6
|
+
* it. SECURITY: the `api` arg / EXTENSION_DEV_API_URL is operator-supplied, but a
|
|
7
|
+
* hostile value (e.g. via prompt-injection in the client) could redirect the
|
|
8
|
+
* token to an attacker. The access token must never leave over plaintext or go to
|
|
9
|
+
* an arbitrary scheme, so we require https -- allowing http only for localhost
|
|
10
|
+
* dev. Returns the normalized base (no trailing slash) or an error message.
|
|
11
|
+
*/
|
|
12
|
+
export declare function safeApiBase(raw: string): {
|
|
13
|
+
ok: true;
|
|
14
|
+
base: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
export interface LoginConfig {
|
|
20
|
+
clientId: string;
|
|
21
|
+
scope: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the GitHub OAuth client id the device flow authenticates against.
|
|
25
|
+
* EXTENSION_DEV_GITHUB_CLIENT_ID overrides (useful when pointing at a self-
|
|
26
|
+
* hosted platform); otherwise it comes from the platform's public config.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fetchLoginConfig(apiBase: string, fetchImpl?: FetchImpl): Promise<LoginConfig>;
|
|
29
|
+
/**
|
|
30
|
+
* Trade a verified GitHub user token for a project-scoped access token and
|
|
31
|
+
* write it to the local credentials file. Returns the stored credentials
|
|
32
|
+
* (token included for the caller's in-memory use; never logged).
|
|
33
|
+
*/
|
|
34
|
+
export declare function exchangeAndPersist(args: {
|
|
35
|
+
apiBase: string;
|
|
36
|
+
githubToken: string;
|
|
37
|
+
project: string;
|
|
38
|
+
fetchImpl?: FetchImpl;
|
|
39
|
+
}): Promise<StoredCredentials>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type FetchImpl = typeof fetch;
|
|
2
|
+
/** Resolve the access token: EXTENSION_DEV_TOKEN env first, then the login creds file. */
|
|
3
|
+
export declare function resolveToken(): string;
|
|
4
|
+
export interface PublishOptions {
|
|
5
|
+
/** Share-link lifetime in hours (1-168, platform default 24). */
|
|
6
|
+
ttlHours?: number;
|
|
7
|
+
/** Pin the share URL to a specific build sha. */
|
|
8
|
+
buildSha?: string;
|
|
9
|
+
/** Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). */
|
|
10
|
+
api?: string;
|
|
11
|
+
/** Explicit token; defaults to resolveToken(). */
|
|
12
|
+
token?: string;
|
|
13
|
+
fetchImpl?: FetchImpl;
|
|
14
|
+
}
|
|
15
|
+
export type PublishResult = {
|
|
16
|
+
ok: true;
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: {
|
|
21
|
+
name: string;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Publish the project the token is scoped to and return the platform
|
|
27
|
+
* response (shareUrl, visibility, ...). The publish target is identified by
|
|
28
|
+
* the token's claims (workspaceSlug/projectSlug), so no project path is sent.
|
|
29
|
+
*/
|
|
30
|
+
export declare function publish(options?: PublishOptions): Promise<PublishResult>;
|
|
31
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.5.0",
|
|
5
5
|
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 30 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
|
|
6
6
|
"mcpName": "io.github.extensiondev/mcp",
|
|
7
7
|
"license": "MIT",
|
|
@@ -84,7 +84,6 @@
|
|
|
84
84
|
"extension.js"
|
|
85
85
|
],
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@extension.dev/core": "^0.2.0",
|
|
88
87
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
89
88
|
"browser-extension-manifest-fields": "^2.2.9",
|
|
90
89
|
"cross-spawn": "^7.0.6",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.5.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@extension.dev/mcp",
|
|
16
|
-
"version": "4.
|
|
16
|
+
"version": "4.5.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|