@extension.dev/mcp 4.4.0 → 4.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.
@@ -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.4.0",
13
+ "version": "4.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": "4.4.0",
4
+ "version": "4.6.0",
5
5
  "author": {
6
6
  "name": "Cezar Augusto",
7
7
  "email": "boss@cezaraugusto.net",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.6.0
4
+
5
+ New `extension_deploy` tool (31 tools total): submit a built extension to the
6
+ Chrome Web Store, Firefox AMO, and Edge Add-ons by driving the standalone
7
+ `@extension.dev/deploy` CLI. Store targets are inferred from the `.zip` paths
8
+ you pass. It defaults to a dry run, and store credentials are read from the
9
+ environment or a `.env.submit` file, never from tool arguments, so secrets
10
+ never enter the agent transcript.
11
+
12
+ ## 4.5.0
13
+
14
+ The platform client (GitHub device-code login, the credential store, and the
15
+ publish flow) is now vendored directly in this package instead of the
16
+ separate `@extension.dev/core` dependency. No behavior change: the tool
17
+ schemas, the credential file, token resolution, and the publish error
18
+ envelopes are all unchanged. This drops a runtime dependency and the
19
+ two-package release step.
20
+
3
21
  ## 4.4.0
4
22
 
5
23
  Browser-matrix parity release: the tool surface now mirrors the engine
package/README.md CHANGED
@@ -121,12 +121,13 @@ cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/c
121
121
  | platform | `extension_logout` | Remove stored credentials |
122
122
  | platform | `extension_publish` | Publish a shareable preview to extension.dev |
123
123
  | platform | `extension_release_promote` | Promote a build to a release channel, headless |
124
+ | platform | `extension_deploy` | Submit to the Chrome, Firefox, and Edge stores (wraps `@extension.dev/deploy`) |
124
125
 
125
126
  Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension` CLI — the project's own `node_modules/.bin/extension` when present, otherwise `npx extension@<pinned>` at the version this package is verified against; everything else runs in-process.
126
127
 
127
128
  ## From preview to store
128
129
 
129
- The four platform tools connect agents to [extension.dev](https://extension.dev): `extension_login` runs a GitHub device-code flow and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build into a shareable preview URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. To automate store submission itself (Chrome Web Store, Edge Add-ons, Firefox AMO), pair with [`@extension.dev/deploy`](https://www.npmjs.com/package/@extension.dev/deploy).
130
+ The platform tools connect agents to [extension.dev](https://extension.dev): `extension_login` runs a GitHub device-code flow and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build into a shareable preview URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. `extension_deploy` submits the built `.zip` to the Chrome Web Store, Edge Add-ons, and Firefox AMO by driving the standalone [`@extension.dev/deploy`](https://www.npmjs.com/package/@extension.dev/deploy) CLI — it defaults to a dry run and reads store credentials from the environment or a `.env.submit` file, never from tool arguments.
130
131
 
131
132
  ## The extension.dev stack
132
133
 
@@ -134,7 +135,6 @@ The four platform tools connect agents to [extension.dev](https://extension.dev)
134
135
  | --- | --- |
135
136
  | [`@extension.dev/skill`](https://www.npmjs.com/package/@extension.dev/skill) | Teach AI agents the judgment half: cross-browser rules, gotchas, playbooks |
136
137
  | [`@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
138
 
139
139
  All of it rides on [Extension.js](https://github.com/extension-js/extension.js), the open-source cross-browser extension framework.
140
140
 
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";
@@ -33,6 +32,13 @@ __webpack_require__.d(create_namespaceObject, {
33
32
  handler: ()=>create_handler,
34
33
  schema: ()=>create_schema
35
34
  });
35
+ var deploy_namespaceObject = {};
36
+ __webpack_require__.r(deploy_namespaceObject);
37
+ __webpack_require__.d(deploy_namespaceObject, {
38
+ buildDeployArgs: ()=>buildDeployArgs,
39
+ handler: ()=>deploy_handler,
40
+ schema: ()=>deploy_schema
41
+ });
36
42
  var detect_browsers_namespaceObject = {};
37
43
  __webpack_require__.r(detect_browsers_namespaceObject);
38
44
  __webpack_require__.d(detect_browsers_namespaceObject, {
@@ -135,9 +141,9 @@ __webpack_require__.d(preview_namespaceObject, {
135
141
  handler: ()=>preview_handler,
136
142
  schema: ()=>preview_schema
137
143
  });
138
- var publish_namespaceObject = {};
139
- __webpack_require__.r(publish_namespaceObject);
140
- __webpack_require__.d(publish_namespaceObject, {
144
+ var tools_publish_namespaceObject = {};
145
+ __webpack_require__.r(tools_publish_namespaceObject);
146
+ __webpack_require__.d(tools_publish_namespaceObject, {
141
147
  handler: ()=>publish_handler,
142
148
  schema: ()=>publish_schema
143
149
  });
@@ -195,7 +201,7 @@ __webpack_require__.d(whoami_namespaceObject, {
195
201
  handler: ()=>whoami_handler,
196
202
  schema: ()=>whoami_schema
197
203
  });
198
- var package_namespaceObject = JSON.parse('{"rE":"4.4.0","El":{"OP":"^4.0.11"}}');
204
+ var package_namespaceObject = JSON.parse('{"rE":"4.5.0","El":{"OP":"^4.0.11"}}');
199
205
  const create_schema = {
200
206
  name: "extension_create",
201
207
  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 +2796,222 @@ async function dom_inspect_handler(args) {
2790
2796
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2791
2797
  return runActVerb(cli, args.projectPath, args.timeout);
2792
2798
  }
2799
+ function credentialsPath() {
2800
+ if ("win32" === process.platform) {
2801
+ const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
2802
+ return node_path.join(base, "extension-dev", "auth.json");
2803
+ }
2804
+ const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
2805
+ const base = xdg || node_path.join(node_os.homedir(), ".config");
2806
+ return node_path.join(base, "extension-dev", "auth.json");
2807
+ }
2808
+ function readCredentials() {
2809
+ try {
2810
+ const raw = node_fs.readFileSync(credentialsPath(), "utf8");
2811
+ const data = JSON.parse(raw);
2812
+ if (!data || "object" != typeof data) return null;
2813
+ if (1 !== data.version) return null;
2814
+ const token = String(data.token || "").trim();
2815
+ if (!token) return null;
2816
+ return {
2817
+ version: 1,
2818
+ token,
2819
+ workspaceSlug: String(data.workspaceSlug || ""),
2820
+ projectSlug: String(data.projectSlug || ""),
2821
+ expiresAt: Number(data.expiresAt || 0),
2822
+ api: String(data.api || "")
2823
+ };
2824
+ } catch {
2825
+ return null;
2826
+ }
2827
+ }
2828
+ function writeCredentials(creds) {
2829
+ const file = credentialsPath();
2830
+ const dir = node_path.dirname(file);
2831
+ node_fs.mkdirSync(dir, {
2832
+ recursive: true,
2833
+ mode: 448
2834
+ });
2835
+ try {
2836
+ node_fs.chmodSync(dir, 448);
2837
+ } catch {}
2838
+ node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
2839
+ mode: 384
2840
+ });
2841
+ try {
2842
+ node_fs.chmodSync(file, 384);
2843
+ } catch {}
2844
+ return file;
2845
+ }
2846
+ function clearCredentials() {
2847
+ const file = credentialsPath();
2848
+ try {
2849
+ node_fs.unlinkSync(file);
2850
+ return {
2851
+ cleared: true,
2852
+ path: file
2853
+ };
2854
+ } catch {
2855
+ return {
2856
+ cleared: false,
2857
+ path: file
2858
+ };
2859
+ }
2860
+ }
2861
+ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
2862
+ const creds = readCredentials();
2863
+ if (!creds) return null;
2864
+ if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
2865
+ return creds;
2866
+ }
2867
+ const DEFAULT_API = "https://www.extension.dev";
2868
+ function resolveApiBase(api) {
2869
+ return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
2870
+ }
2871
+ function safeApiBase(raw) {
2872
+ let parsed;
2873
+ try {
2874
+ parsed = new URL(raw);
2875
+ } catch {
2876
+ return {
2877
+ ok: false,
2878
+ message: `Invalid platform URL: ${raw}`
2879
+ };
2880
+ }
2881
+ const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
2882
+ if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
2883
+ ok: false,
2884
+ message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
2885
+ };
2886
+ return {
2887
+ ok: true,
2888
+ base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
2889
+ };
2890
+ }
2891
+ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
2892
+ const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
2893
+ if (override) return {
2894
+ clientId: override,
2895
+ scope: "read:user"
2896
+ };
2897
+ const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
2898
+ headers: {
2899
+ accept: "application/json"
2900
+ }
2901
+ });
2902
+ if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
2903
+ const data = await res.json().catch(()=>({}));
2904
+ const clientId = String(data.githubClientId || "").trim();
2905
+ if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
2906
+ return {
2907
+ clientId,
2908
+ scope: String(data.scope || "read:user")
2909
+ };
2910
+ }
2911
+ async function exchangeAndPersist(args) {
2912
+ const doFetch = args.fetchImpl ?? fetch;
2913
+ const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
2914
+ method: "POST",
2915
+ headers: {
2916
+ "content-type": "application/json",
2917
+ accept: "application/json"
2918
+ },
2919
+ body: JSON.stringify({
2920
+ githubToken: args.githubToken,
2921
+ project: args.project
2922
+ })
2923
+ });
2924
+ const text = await res.text();
2925
+ let data;
2926
+ try {
2927
+ data = JSON.parse(text);
2928
+ } catch {
2929
+ data = {
2930
+ message: text
2931
+ };
2932
+ }
2933
+ if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
2934
+ const token = String(data.token || "").trim();
2935
+ if (!token) throw new Error("Login exchange returned no token.");
2936
+ const creds = {
2937
+ version: 1,
2938
+ token,
2939
+ workspaceSlug: String(data.workspaceSlug || ""),
2940
+ projectSlug: String(data.projectSlug || ""),
2941
+ expiresAt: Number(data.expiresAt || 0),
2942
+ api: args.apiBase
2943
+ };
2944
+ writeCredentials(creds);
2945
+ return creds;
2946
+ }
2947
+ function resolveToken() {
2948
+ const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
2949
+ if (fromEnv) return fromEnv;
2950
+ const creds = readValidCredentials();
2951
+ return creds?.token ? String(creds.token).trim() : "";
2952
+ }
2953
+ async function publish(options = {}) {
2954
+ const token = options.token ?? resolveToken();
2955
+ if (!token) return {
2956
+ ok: false,
2957
+ error: {
2958
+ name: "PublishAuthError",
2959
+ message: "No token. Run login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
2960
+ }
2961
+ };
2962
+ const doFetch = options.fetchImpl ?? fetch;
2963
+ const apiCheck = safeApiBase(resolveApiBase(options.api));
2964
+ if (!apiCheck.ok) return {
2965
+ ok: false,
2966
+ error: {
2967
+ name: "PublishConfigError",
2968
+ message: apiCheck.message
2969
+ }
2970
+ };
2971
+ const url = `${apiCheck.base}/api/cli/publish`;
2972
+ const body = {};
2973
+ if (null != options.ttlHours) body.ttlHours = Number(options.ttlHours);
2974
+ if (options.buildSha) body.buildSha = options.buildSha;
2975
+ let res;
2976
+ try {
2977
+ res = await doFetch(url, {
2978
+ method: "POST",
2979
+ headers: {
2980
+ authorization: `Bearer ${token}`,
2981
+ "content-type": "application/json"
2982
+ },
2983
+ body: JSON.stringify(body)
2984
+ });
2985
+ } catch (err) {
2986
+ return {
2987
+ ok: false,
2988
+ error: {
2989
+ name: "PublishNetworkError",
2990
+ message: `Could not reach ${url}: ${err?.message || err}`
2991
+ }
2992
+ };
2993
+ }
2994
+ const text = await res.text();
2995
+ let data;
2996
+ try {
2997
+ data = JSON.parse(text);
2998
+ } catch {
2999
+ data = {
3000
+ message: text
3001
+ };
3002
+ }
3003
+ if (!res.ok) return {
3004
+ ok: false,
3005
+ error: {
3006
+ name: "PublishError",
3007
+ message: `publish failed (${res.status}): ${data?.message || text || "unknown error"}`
3008
+ }
3009
+ };
3010
+ return {
3011
+ ok: true,
3012
+ data
3013
+ };
3014
+ }
2793
3015
  const publish_schema = {
2794
3016
  name: "extension_publish",
2795
3017
  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 +3057,7 @@ async function publish_handler(args) {
2835
3057
  });
2836
3058
  return result.ok ? JSON.stringify(result.data) : JSON.stringify(result);
2837
3059
  }
2838
- const DEFAULT_API = "https://www.extension.dev";
3060
+ const release_promote_DEFAULT_API = "https://www.extension.dev";
2839
3061
  const release_promote_schema = {
2840
3062
  name: "extension_release_promote",
2841
3063
  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 +3117,7 @@ async function release_promote_handler(args) {
2895
3117
  const buildId = String(args.buildId || "").trim();
2896
3118
  const channel = String(args.channel || "").trim();
2897
3119
  if (!buildId || !channel) return fail("ReleaseInputError", "buildId and channel are required.");
2898
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
3120
+ const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
2899
3121
  if (!apiCheck.ok) return fail("ReleaseConfigError", apiCheck.message);
2900
3122
  const url = `${apiCheck.base}/api/cli/release/promote`;
2901
3123
  const body = {
@@ -2931,6 +3153,169 @@ async function release_promote_handler(args) {
2931
3153
  if (!res.ok) return fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
2932
3154
  return JSON.stringify(data);
2933
3155
  }
3156
+ const DEFAULT_DEPLOY_VERSION = "1.2.1";
3157
+ function pinnedDeployVersion() {
3158
+ return String(process.env.EXTENSION_DEPLOY_VERSION || "").trim() || DEFAULT_DEPLOY_VERSION;
3159
+ }
3160
+ function buildDeployArgs(args) {
3161
+ const argv = [];
3162
+ const pushValue = (flag, value)=>{
3163
+ if (void 0 === value || "" === value) return;
3164
+ argv.push(flag, String(value));
3165
+ };
3166
+ if (false !== args.dryRun) argv.push("--dry-run");
3167
+ pushValue("--chrome-zip", args.chromeZip);
3168
+ pushValue("--chrome-extension-id", args.chromeExtensionId);
3169
+ pushValue("--chrome-publisher-id", args.chromePublisherId);
3170
+ if (args.stagedPublish) argv.push("--chrome-staged-publish");
3171
+ pushValue("--chrome-deploy-percentage", args.chromeDeployPercentage);
3172
+ if (args.chromeSkipSubmitReview) argv.push("--chrome-skip-submit-review");
3173
+ pushValue("--firefox-zip", args.firefoxZip);
3174
+ pushValue("--firefox-sources-zip", args.firefoxSourcesZip);
3175
+ pushValue("--firefox-extension-id", args.firefoxExtensionId);
3176
+ pushValue("--firefox-channel", args.firefoxChannel);
3177
+ pushValue("--edge-zip", args.edgeZip);
3178
+ pushValue("--edge-product-id", args.edgeProductId);
3179
+ if (args.edgeSkipSubmitReview) argv.push("--edge-skip-submit-review");
3180
+ pushValue("--output-json", args.outputJson);
3181
+ return argv;
3182
+ }
3183
+ const deploy_schema = {
3184
+ name: "extension_deploy",
3185
+ description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons by driving the standalone @extension.dev/deploy CLI. Provide the built .zip path(s); the target stores are inferred from which zips you pass. DEFAULTS TO A DRY RUN (verifies auth and inputs, uploads nothing) - pass dryRun:false to actually submit, which is irreversible and enters store review. Store CREDENTIALS are NOT arguments: set them in the environment or a .env.submit file in projectPath (CHROME_CLIENT_ID/CHROME_CLIENT_SECRET/CHROME_REFRESH_TOKEN or CHROME_SERVICE_ACCOUNT_JSON; FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET; EDGE_CLIENT_ID/EDGE_API_KEY). Runs `npx @extension.dev/deploy`.",
3186
+ inputSchema: {
3187
+ type: "object",
3188
+ properties: {
3189
+ projectPath: {
3190
+ type: "string",
3191
+ description: "Working directory. Relative zip paths and a .env.submit credential file are resolved from here."
3192
+ },
3193
+ chromeZip: {
3194
+ type: "string",
3195
+ description: "Path to the built Chrome extension .zip"
3196
+ },
3197
+ chromeExtensionId: {
3198
+ type: "string",
3199
+ description: "Chrome extension ID (or set CHROME_EXTENSION_ID)"
3200
+ },
3201
+ chromePublisherId: {
3202
+ type: "string",
3203
+ description: "Chrome publisher UUID (or set CHROME_PUBLISHER_ID)"
3204
+ },
3205
+ firefoxZip: {
3206
+ type: "string",
3207
+ description: "Path to the built Firefox extension .zip"
3208
+ },
3209
+ firefoxSourcesZip: {
3210
+ type: "string",
3211
+ description: "Path to the Firefox sources .zip (optional)"
3212
+ },
3213
+ firefoxExtensionId: {
3214
+ type: "string",
3215
+ description: "Firefox add-on GUID or email-style ID"
3216
+ },
3217
+ firefoxChannel: {
3218
+ type: "string",
3219
+ enum: [
3220
+ "listed",
3221
+ "unlisted"
3222
+ ],
3223
+ description: "Firefox channel (unlisted is required for a first submission)"
3224
+ },
3225
+ edgeZip: {
3226
+ type: "string",
3227
+ description: "Path to the built Edge extension .zip"
3228
+ },
3229
+ edgeProductId: {
3230
+ type: "string",
3231
+ description: "Edge Partner Center product ID (or set EDGE_PRODUCT_ID)"
3232
+ },
3233
+ dryRun: {
3234
+ type: "boolean",
3235
+ default: true,
3236
+ description: "Verify auth and inputs without uploading or publishing. Pass false to actually submit (irreversible)."
3237
+ },
3238
+ stagedPublish: {
3239
+ type: "boolean",
3240
+ default: false,
3241
+ description: "Chrome: publish as STAGED_PUBLISH so the rollout can be raised review-free later."
3242
+ },
3243
+ chromeDeployPercentage: {
3244
+ type: "number",
3245
+ description: "Chrome staged rollout percentage (1-100)."
3246
+ },
3247
+ chromeSkipSubmitReview: {
3248
+ type: "boolean",
3249
+ default: false,
3250
+ description: "Chrome: upload only, skip the publish/submit-for-review step."
3251
+ },
3252
+ edgeSkipSubmitReview: {
3253
+ type: "boolean",
3254
+ default: false,
3255
+ description: "Edge: upload only, skip the publish/submit-for-review step."
3256
+ },
3257
+ outputJson: {
3258
+ type: "string",
3259
+ description: "Write the machine-readable DeployResult JSON to this path."
3260
+ }
3261
+ },
3262
+ required: [
3263
+ "projectPath"
3264
+ ]
3265
+ }
3266
+ };
3267
+ async function deploy_handler(args) {
3268
+ if (!args.chromeZip && !args.firefoxZip && !args.edgeZip) return JSON.stringify({
3269
+ ok: false,
3270
+ error: {
3271
+ name: "DeployInputError",
3272
+ message: "No store targets. Provide at least one of chromeZip, firefoxZip, or edgeZip."
3273
+ }
3274
+ });
3275
+ const version = pinnedDeployVersion();
3276
+ const argv = [
3277
+ "--yes",
3278
+ `@extension.dev/deploy@${version}`,
3279
+ ...buildDeployArgs(args)
3280
+ ];
3281
+ return new Promise((resolve)=>{
3282
+ const child = cross_spawn("npx", argv, {
3283
+ cwd: args.projectPath,
3284
+ stdio: [
3285
+ "ignore",
3286
+ "pipe",
3287
+ "pipe"
3288
+ ],
3289
+ env: {
3290
+ ...process.env,
3291
+ FORCE_COLOR: "0",
3292
+ NO_COLOR: "1"
3293
+ }
3294
+ });
3295
+ let stdout = "";
3296
+ let stderr = "";
3297
+ child.stdout?.on("data", (d)=>stdout += d.toString());
3298
+ child.stderr?.on("data", (d)=>stderr += d.toString());
3299
+ child.on("close", (code)=>{
3300
+ resolve(JSON.stringify({
3301
+ ok: 0 === code,
3302
+ dryRun: false !== args.dryRun,
3303
+ code,
3304
+ stdout: stdout.trim(),
3305
+ stderr: stderr.trim()
3306
+ }));
3307
+ });
3308
+ child.on("error", (err)=>{
3309
+ resolve(JSON.stringify({
3310
+ ok: false,
3311
+ error: {
3312
+ name: "DeploySpawnError",
3313
+ message: `Could not run extension-deploy: ${err instanceof Error ? err.message : String(err)}`
3314
+ }
3315
+ }));
3316
+ });
3317
+ });
3318
+ }
2934
3319
  const wait_schema = {
2935
3320
  name: "extension_wait",
2936
3321
  description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
@@ -3225,6 +3610,78 @@ async function add_feature_handler(args) {
3225
3610
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
3226
3611
  });
3227
3612
  }
3613
+ const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
3614
+ const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
3615
+ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
3616
+ const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
3617
+ async function startDeviceCode(args) {
3618
+ const doFetch = args.fetchImpl ?? fetch;
3619
+ const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
3620
+ method: "POST",
3621
+ headers: {
3622
+ accept: "application/json",
3623
+ "content-type": "application/json"
3624
+ },
3625
+ body: JSON.stringify({
3626
+ client_id: args.clientId,
3627
+ scope: args.scope || "read:user"
3628
+ })
3629
+ });
3630
+ const data = await res.json().catch(()=>({}));
3631
+ if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
3632
+ return {
3633
+ deviceCode: String(data.device_code || ""),
3634
+ userCode: String(data.user_code || ""),
3635
+ verificationUri: String(data.verification_uri || "https://github.com/login/device"),
3636
+ interval: Number(data.interval || 5),
3637
+ expiresIn: Number(data.expires_in || 900)
3638
+ };
3639
+ }
3640
+ async function pollForToken(args) {
3641
+ const doFetch = args.fetchImpl ?? fetch;
3642
+ const sleep = args.sleepImpl ?? defaultSleep;
3643
+ let intervalMs = 1000 * Math.max(1, args.interval);
3644
+ const deadline = Date.now() + Math.max(0, args.budgetMs);
3645
+ while(Date.now() < deadline){
3646
+ await sleep(intervalMs);
3647
+ const res = await doFetch(GITHUB_TOKEN_URL, {
3648
+ method: "POST",
3649
+ headers: {
3650
+ accept: "application/json",
3651
+ "content-type": "application/json"
3652
+ },
3653
+ body: JSON.stringify({
3654
+ client_id: args.clientId,
3655
+ device_code: args.deviceCode,
3656
+ grant_type: DEVICE_GRANT_TYPE
3657
+ })
3658
+ });
3659
+ const data = await res.json().catch(()=>({}));
3660
+ const token = String(data.access_token || "").trim();
3661
+ if (token) return {
3662
+ ok: true,
3663
+ githubToken: token
3664
+ };
3665
+ const error = String(data.error || "");
3666
+ if ("authorization_pending" === error) continue;
3667
+ if ("slow_down" === error) {
3668
+ intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
3669
+ continue;
3670
+ }
3671
+ if ("expired_token" === error) return {
3672
+ ok: false,
3673
+ reason: "expired"
3674
+ };
3675
+ if ("access_denied" === error) return {
3676
+ ok: false,
3677
+ reason: "denied"
3678
+ };
3679
+ }
3680
+ return {
3681
+ ok: false,
3682
+ reason: "pending"
3683
+ };
3684
+ }
3228
3685
  const login_schema = {
3229
3686
  name: "extension_login",
3230
3687
  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,8 +4463,9 @@ const tools = [
4006
4463
  reload_namespaceObject,
4007
4464
  open_namespaceObject,
4008
4465
  dom_inspect_namespaceObject,
4009
- publish_namespaceObject,
4466
+ tools_publish_namespaceObject,
4010
4467
  release_promote_namespaceObject,
4468
+ deploy_namespaceObject,
4011
4469
  wait_namespaceObject,
4012
4470
  add_feature_namespaceObject,
4013
4471
  login_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 {};
@@ -0,0 +1,104 @@
1
+ export interface DeployToolArgs {
2
+ projectPath: string;
3
+ chromeZip?: string;
4
+ chromeExtensionId?: string;
5
+ chromePublisherId?: string;
6
+ firefoxZip?: string;
7
+ firefoxSourcesZip?: string;
8
+ firefoxExtensionId?: string;
9
+ firefoxChannel?: "listed" | "unlisted";
10
+ edgeZip?: string;
11
+ edgeProductId?: string;
12
+ dryRun?: boolean;
13
+ stagedPublish?: boolean;
14
+ chromeDeployPercentage?: number;
15
+ chromeSkipSubmitReview?: boolean;
16
+ edgeSkipSubmitReview?: boolean;
17
+ outputJson?: string;
18
+ }
19
+ /**
20
+ * Build the extension-deploy argv from tool arguments. Pure and exported so the
21
+ * flag mapping is unit-tested without spawning a process. Never emits a
22
+ * credential flag - secrets come from the environment / .env.submit.
23
+ */
24
+ export declare function buildDeployArgs(args: DeployToolArgs): string[];
25
+ export declare const schema: {
26
+ name: string;
27
+ description: string;
28
+ inputSchema: {
29
+ type: "object";
30
+ properties: {
31
+ projectPath: {
32
+ type: string;
33
+ description: string;
34
+ };
35
+ chromeZip: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ chromeExtensionId: {
40
+ type: string;
41
+ description: string;
42
+ };
43
+ chromePublisherId: {
44
+ type: string;
45
+ description: string;
46
+ };
47
+ firefoxZip: {
48
+ type: string;
49
+ description: string;
50
+ };
51
+ firefoxSourcesZip: {
52
+ type: string;
53
+ description: string;
54
+ };
55
+ firefoxExtensionId: {
56
+ type: string;
57
+ description: string;
58
+ };
59
+ firefoxChannel: {
60
+ type: string;
61
+ enum: string[];
62
+ description: string;
63
+ };
64
+ edgeZip: {
65
+ type: string;
66
+ description: string;
67
+ };
68
+ edgeProductId: {
69
+ type: string;
70
+ description: string;
71
+ };
72
+ dryRun: {
73
+ type: string;
74
+ default: boolean;
75
+ description: string;
76
+ };
77
+ stagedPublish: {
78
+ type: string;
79
+ default: boolean;
80
+ description: string;
81
+ };
82
+ chromeDeployPercentage: {
83
+ type: string;
84
+ description: string;
85
+ };
86
+ chromeSkipSubmitReview: {
87
+ type: string;
88
+ default: boolean;
89
+ description: string;
90
+ };
91
+ edgeSkipSubmitReview: {
92
+ type: string;
93
+ default: boolean;
94
+ description: string;
95
+ };
96
+ outputJson: {
97
+ type: string;
98
+ description: string;
99
+ };
100
+ };
101
+ required: string[];
102
+ };
103
+ };
104
+ export declare function handler(args: DeployToolArgs): Promise<string>;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "4.4.0",
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.",
4
+ "version": "4.6.0",
5
+ "description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 31 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, Safari, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf). Powered by extension.dev and Extension.js.",
6
6
  "mcpName": "io.github.extensiondev/mcp",
7
7
  "license": "MIT",
8
8
  "author": {
@@ -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
@@ -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. 30 tools, 11 browsers.",
4
+ "description": "Build, run, inspect, and publish browser extensions from any MCP client. 31 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": "4.4.0",
10
+ "version": "4.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": "4.4.0",
16
+ "version": "4.6.0",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"