@extension.dev/mcp 4.0.8 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.1
4
+
5
+ README rewritten for the public npm page: Extension.js-style header
6
+ (badges, tagline, quick start), tool table updated to the full 27-tool
7
+ surface (release-promote was missing), and links to private repos or
8
+ npm-restricted packages removed. Package description tool count fixed
9
+ (26 -> 27). No code changes.
10
+
11
+ ## 4.1.0
12
+
13
+ The MCP now consumes `@extension.dev/core` for all platform-auth logic
14
+ (core MIGRATION.md phase 2). No tool schema changes, no behavior changes:
15
+ the JSON-string envelopes are byte-compatible and pinned by tests.
16
+
17
+ - New dependency `@extension.dev/core` ^0.2.0: device-code login, credential
18
+ store, and publish client now live there, shared with every other surface.
19
+ - Deleted `src/lib/credentials.ts`, `src/lib/github-device.ts`,
20
+ `src/lib/login-flow.ts` and their migrated tests; `login`, `whoami`,
21
+ `logout`, and `release-promote` import from core.
22
+ - `tools/publish.ts` is a thin adapter over core's `publish()`; the frozen
23
+ PublishAuthError / PublishConfigError / PublishNetworkError / PublishError
24
+ envelopes and the success passthrough are pinned by a new
25
+ `publish-envelope` test.
26
+ - New `core-boundary` regression test: no file under `src/` may redefine the
27
+ credential store or import auth primitives from anywhere but
28
+ `@extension.dev/core`.
29
+ - CI and Release workflows pass `NPM_TOKEN` to the install step (core is
30
+ npm-restricted until the public flip).
31
+
3
32
  ## 4.0.8
4
33
 
5
34
  Tracks the `extension` 4.0.8 suite (the versioning convention: this package's
package/README.md CHANGED
@@ -1,48 +1,37 @@
1
- [npm-version-image]: https://img.shields.io/npm/v/%40extension.dev%2Fmcp
1
+ [npm-version-image]: https://img.shields.io/npm/v/%40extension.dev%2Fmcp.svg?color=0971fe
2
2
  [npm-version-url]: https://www.npmjs.com/package/@extension.dev/mcp
3
- [action-image]: https://github.com/extensiondev/mcp/actions/workflows/ci.yml/badge.svg?branch=main
4
- [action-url]: https://github.com/extensiondev/mcp/actions
3
+ [npm-downloads-image]: https://img.shields.io/npm/dm/%40extension.dev%2Fmcp.svg?color=0971fe
4
+ [npm-downloads-url]: https://www.npmjs.com/package/@extension.dev/mcp
5
+ [discord-image]: https://img.shields.io/discord/1253608412890271755?label=Discord&logo=discord&style=flat&color=0971fe
6
+ [discord-url]: https://discord.gg/v9h2RgeTSN
5
7
 
6
- [![Version][npm-version-image]][npm-version-url] [![workflow][action-image]][action-url]
8
+ # @extension.dev/mcp [![Version][npm-version-image]][npm-version-url] [![Downloads][npm-downloads-image]][npm-downloads-url] [![Discord][discord-image]][discord-url]
7
9
 
8
- # @extension.dev/mcp
10
+ > Give your AI agent hands for browser extension development. 27 MCP tools that scaffold, run, inspect, debug, and publish cross-browser extensions.
9
11
 
10
- Give your AI agent hands for browser extension development. One command connects Claude Code, Claude Desktop, Cursor, or any MCP client to 26 tools that scaffold, run, inspect, debug, and publish cross-browser extensions:
12
+ <img alt="Logo" align="right" src="https://avatars.githubusercontent.com/u/106714027" width="20%" />
11
13
 
12
14
  ```bash
13
15
  claude mcp add extension-dev npx @extension.dev/mcp
14
16
  ```
15
17
 
16
- Extensions fail silently: content scripts that never inject, panels that never open, permissions that return `undefined` with no error. These tools give agents eyes on the live browser (DOM probes, unified logs from every context, storage access, event replay) so they debug from evidence instead of guessing. Scaffolding draws on the 60+ template catalog of the [extension.dev](https://extension.dev) platform, built on [Extension.js](https://extension.js.org).
18
+ Works with Claude Code, Claude Desktop, Cursor, and any MCP client.
17
19
 
18
- ## What's in this package
20
+ [extension.dev](https://extension.dev) · [Documentation](https://extension.js.org) · [Templates](https://templates.extension.dev) · [Examples](https://github.com/extension-js/examples) · [Discord](https://discord.gg/v9h2RgeTSN)
19
21
 
20
- ```
21
- @extension.dev/mcp
22
- src/ MCP server source (26 tools)
23
- claude/ Claude Code integration (CLAUDE.md, slash commands, rules)
24
- bin/ CLI entrypoint
25
- ```
26
-
27
- **MCP server**: programmatic bridge between AI assistants and the extension.dev platform. Imports from published npm packages:
22
+ ## Why an MCP server for extensions
28
23
 
29
- - `extension-create`: project scaffolding
30
- - `extension-develop`: build/dev/preview
31
- - `extension-install`: managed browser binaries
24
+ Extensions fail silently: content scripts that never inject, panels that never open, permissions that return `undefined` with no error. An agent editing files blind will happily "fix" all of them without noticing none of them work.
32
25
 
33
- **Claude Code integration**: drop-in instructions, slash commands, and rules for Claude Code:
26
+ These tools give agents eyes on the live browser, so they debug from evidence instead of guessing:
34
27
 
35
- - `claude/CLAUDE.md`: project-level instructions for any extension project
36
- - `claude/commands/`: slash commands (`/extension`, `/extension-add`, `/extension-debug`, `/extension-publish`)
37
- - `claude/rules/`: rules for extension development, cross-browser compat, and MCP tools
28
+ - **Scaffold** from the 60+ template catalog behind [templates.extension.dev](https://templates.extension.dev), or add a popup, sidebar, or content script to an existing project
29
+ - **Run** the dev server with HMR in Chrome, Edge, Firefox, or any managed browser binary, no build config
30
+ - **See** the live DOM, unified logs from every extension context, `chrome.storage` contents, and the loaded-extension list
31
+ - **Act**: evaluate code in any context, trigger the action button and commands, reload the extension, replay events
32
+ - **Ship**: validate the manifest cross-browser, build for production, publish a shareable preview, and promote builds to release channels headlessly
38
33
 
39
- **Agent Skill**: the knowledge companion to this server lives in [`@extension.dev/skill`](https://npmjs.com/package/@extension.dev/skill): a portable [Agent Skills](https://agentskills.io)-format skill (SKILL.md + references) covering cross-browser rules, silent-failure gotchas, debugging playbooks, and store publishing. This server gives agents hands; the skill gives them judgment. Pair them:
40
-
41
- ```bash
42
- mkdir -p .claude/skills && cp -R node_modules/@extension.dev/skill/skills/extension-dev .claude/skills/
43
- ```
44
-
45
- Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension` CLI since they require the full browser launcher infrastructure.
34
+ Built on [Extension.js](https://extension.js.org), the open-source cross-browser extension framework.
46
35
 
47
36
  ## Setup
48
37
 
@@ -52,7 +41,7 @@ Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension`
52
41
  claude mcp add extension-dev npx @extension.dev/mcp
53
42
  ```
54
43
 
55
- ### Claude Desktop / .mcp.json
44
+ ### Claude Desktop / `.mcp.json`
56
45
 
57
46
  ```json
58
47
  {
@@ -65,79 +54,63 @@ claude mcp add extension-dev npx @extension.dev/mcp
65
54
  }
66
55
  ```
67
56
 
68
- ### Claude Code integration (manual)
57
+ ### Claude Code project integration
69
58
 
70
- Copy the Claude Code rules and commands into any extension project:
59
+ The package ships drop-in instructions, slash commands, and rules for extension projects:
71
60
 
72
61
  ```bash
73
62
  # Rules (how Claude understands your project)
74
63
  cp node_modules/@extension.dev/mcp/claude/CLAUDE.md ~/my-extension/.claude/CLAUDE.md
75
64
 
76
- # Slash commands
65
+ # Slash commands (/extension, /extension-add, /extension-debug, /extension-publish)
77
66
  mkdir -p ~/my-extension/.claude/commands
78
67
  cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/commands/
79
68
  ```
80
69
 
81
70
  ## Tools
82
71
 
83
- | Tier | Tool | Integration | Description |
84
- | ---- | ------------------------------- | ------------------------------- | --------------------------------- |
85
- | 1 | `extension_create` | `extensionCreate()` | Scaffold from a template |
86
- | 1 | `extension_list_templates` | native | Browse 60+ templates |
87
- | 1 | `extension_build` | `extensionBuild()` | Build for production |
88
- | 1 | `extension_dev` | CLI spawn | Dev server with HMR |
89
- | 1 | `extension_start` | CLI spawn | Build + preview |
90
- | 1 | `extension_preview` | CLI spawn | Preview production build |
91
- | 2 | `extension_get_template_source` | native | Read template source files |
92
- | 2 | `extension_manifest_validate` | native | Cross-browser manifest validation |
93
- | 2 | `extension_inspect` | native | Build output analysis |
94
- | 2 | `extension_source_inspect` | CDP WebSocket | Live DOM inspection |
95
- | 2 | `extension_dom_inspect` | agent bridge | CDP-free DOM snapshot |
96
- | 2 | `extension_list_extensions` | CDP WebSocket | List loaded extensions (Chromium) |
97
- | 2 | `extension_logs` | agent bridge | Stream logs from every context |
98
- | 2 | `extension_wait` | native | Poll ready.json contract |
99
- | 2 | `extension_add_feature` | native | Add sidebar/popup/content script |
100
- | act | `extension_eval` | agent bridge | Eval in a context (`--allow-eval`) |
101
- | act | `extension_storage` | agent bridge | Read/write `chrome.storage` |
102
- | act | `extension_reload` | agent bridge | Reload extension or tab |
103
- | act | `extension_open` | agent bridge | Open surface / trigger `action`,`command` |
104
- | 3 | `extension_install_browser` | `extensionInstall()` | Install managed browser |
105
- | 3 | `extension_list_browsers` | `getManagedBrowsersCacheRoot()` | List managed browsers |
106
- | 3 | `extension_detect_browsers` | native | System browser detection |
107
- | auth | `extension_login` | platform | GitHub device-code stored token |
108
- | auth | `extension_whoami` | native | Show stored login (no token) |
109
- | auth | `extension_logout` | native | Remove stored credentials |
110
- | auth | `extension_publish` | platform | Publish to extension.dev (token) |
111
-
112
- ## Development
113
-
114
- ```bash
115
- pnpm install
116
- pnpm compile # Build with rslib
117
- pnpm test # Run tests
118
- pnpm start # Start MCP server
119
- ```
120
-
121
- ### Publishing
122
-
123
- ```bash
124
- NPM_TOKEN=<token> pnpm publish
125
- ```
126
-
127
- Uses `prepublishOnly` to run tests and compile before publishing.
128
-
129
- ## The extension.dev open source stack
130
-
131
- | Package | Use it to |
132
- | --- | --- |
133
- | [`@extension.dev/skill`](https://github.com/extensiondev/skill) | Teach agents the cross-browser rules and silent-failure gotchas |
134
- | [`@extension.dev/deploy`](https://github.com/extensiondev/deploy) | Ship to Chrome, Firefox, and Edge stores from CI |
135
- | [`@extension.dev/artifact-integrity`](https://github.com/extensiondev/artifact-integrity) | Gate releases on artifact verification |
136
- | [`@extension.dev/compiler`](https://github.com/extensiondev/compiler) | Build extensions in the browser with esbuild-wasm |
137
- | [`@extension.dev/core`](https://github.com/extensiondev/core) | Authenticate and publish to the extension.dev platform |
138
-
139
- All of it rides on [Extension.js](https://github.com/extension-js/extension.js), the open-source cross-browser extension framework.
72
+ | Tier | Tool | Description |
73
+ | ---- | ---- | ----------- |
74
+ | build | `extension_create` | Scaffold from a template |
75
+ | build | `extension_list_templates` | Browse 60+ templates |
76
+ | build | `extension_get_template_source` | Read template source files |
77
+ | build | `extension_add_feature` | Add sidebar/popup/content script |
78
+ | build | `extension_build` | Build for production |
79
+ | run | `extension_dev` | Dev server with HMR |
80
+ | run | `extension_start` | Build + preview |
81
+ | run | `extension_preview` | Preview the production build |
82
+ | run | `extension_wait` | Poll the dev-server ready contract |
83
+ | see | `extension_manifest_validate` | Cross-browser manifest validation |
84
+ | see | `extension_inspect` | Build output analysis |
85
+ | see | `extension_source_inspect` | Live DOM inspection (CDP) |
86
+ | see | `extension_dom_inspect` | CDP-free DOM snapshot |
87
+ | see | `extension_list_extensions` | List loaded extensions (Chromium) |
88
+ | see | `extension_logs` | Stream logs from every context |
89
+ | act | `extension_eval` | Evaluate in a context (`--allow-eval`) |
90
+ | act | `extension_storage` | Read/write `chrome.storage` |
91
+ | act | `extension_reload` | Reload extension or tab |
92
+ | act | `extension_open` | Open a surface / trigger `action`, `command` |
93
+ | browsers | `extension_install_browser` | Install a managed browser binary |
94
+ | browsers | `extension_list_browsers` | List managed browsers |
95
+ | browsers | `extension_detect_browsers` | Detect system browsers |
96
+ | platform | `extension_login` | GitHub device-code login, stored token |
97
+ | platform | `extension_whoami` | Show the stored login (never the token) |
98
+ | platform | `extension_logout` | Remove stored credentials |
99
+ | platform | `extension_publish` | Publish a shareable preview to extension.dev |
100
+ | platform | `extension_release_promote` | Promote a build to a release channel, headless |
101
+
102
+ Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension` CLI; everything else runs in-process.
103
+
104
+ ## From preview to store
105
+
106
+ 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).
107
+
108
+ ## Community
109
+
110
+ - Join the [Discord](https://discord.gg/v9h2RgeTSN) for help and feedback
111
+ - Browse production-ready [examples](https://github.com/extension-js/examples)
112
+ - Report Extension.js framework issues on [GitHub](https://github.com/extension-js/extension.js/issues)
140
113
 
141
114
  ## License
142
115
 
143
- MIT
116
+ MIT (c) Cezar Augusto and the Extension.js authors.
package/dist/module.js CHANGED
@@ -11,6 +11,7 @@ import cross_spawn from "cross-spawn";
11
11
  import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
12
12
  import node_net from "node:net";
13
13
  import ws from "ws";
14
+ import { clearCredentials, exchangeAndPersist, fetchLoginConfig, pollForToken, publish, readCredentials, resolveApiBase, resolveToken, safeApiBase, startDeviceCode } from "@extension.dev/core";
14
15
  import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
15
16
  import { execFile } from "node:child_process";
16
17
  import { promisify } from "node:util";
@@ -132,8 +133,6 @@ var publish_namespaceObject = {};
132
133
  __webpack_require__.r(publish_namespaceObject);
133
134
  __webpack_require__.d(publish_namespaceObject, {
134
135
  handler: ()=>publish_handler,
135
- resolveToken: ()=>resolveToken,
136
- safeApiBase: ()=>safeApiBase,
137
136
  schema: ()=>publish_schema
138
137
  });
139
138
  var release_promote_namespaceObject = {};
@@ -179,7 +178,7 @@ __webpack_require__.d(whoami_namespaceObject, {
179
178
  schema: ()=>whoami_schema
180
179
  });
181
180
  var package_namespaceObject = {
182
- rE: "4.0.8"
181
+ rE: "4.1.1"
183
182
  };
184
183
  const schema = {
185
184
  name: "extension_create",
@@ -2341,70 +2340,6 @@ async function dom_inspect_handler(args) {
2341
2340
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2342
2341
  return runActVerb(cli, args.projectPath, args.timeout);
2343
2342
  }
2344
- function credentialsPath() {
2345
- if ("win32" === process.platform) {
2346
- const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
2347
- return node_path.join(base, "extension-dev", "auth.json");
2348
- }
2349
- const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
2350
- const base = xdg || node_path.join(node_os.homedir(), ".config");
2351
- return node_path.join(base, "extension-dev", "auth.json");
2352
- }
2353
- function readCredentials() {
2354
- try {
2355
- const raw = node_fs.readFileSync(credentialsPath(), "utf8");
2356
- const data = JSON.parse(raw);
2357
- if (!data || "object" != typeof data) return null;
2358
- if (1 !== data.version) return null;
2359
- const token = String(data.token || "").trim();
2360
- if (!token) return null;
2361
- return {
2362
- version: 1,
2363
- token,
2364
- workspaceSlug: String(data.workspaceSlug || ""),
2365
- projectSlug: String(data.projectSlug || ""),
2366
- expiresAt: Number(data.expiresAt || 0),
2367
- api: String(data.api || "")
2368
- };
2369
- } catch {
2370
- return null;
2371
- }
2372
- }
2373
- function writeCredentials(creds) {
2374
- const file = credentialsPath();
2375
- node_fs.mkdirSync(node_path.dirname(file), {
2376
- recursive: true
2377
- });
2378
- node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
2379
- mode: 384
2380
- });
2381
- try {
2382
- node_fs.chmodSync(file, 384);
2383
- } catch {}
2384
- return file;
2385
- }
2386
- function clearCredentials() {
2387
- const file = credentialsPath();
2388
- try {
2389
- node_fs.unlinkSync(file);
2390
- return {
2391
- cleared: true,
2392
- path: file
2393
- };
2394
- } catch {
2395
- return {
2396
- cleared: false,
2397
- path: file
2398
- };
2399
- }
2400
- }
2401
- function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
2402
- const creds = readCredentials();
2403
- if (!creds) return null;
2404
- if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
2405
- return creds;
2406
- }
2407
- const DEFAULT_API = "https://www.extension.dev";
2408
2343
  const publish_schema = {
2409
2344
  name: "extension_publish",
2410
2345
  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.",
@@ -2433,76 +2368,24 @@ const publish_schema = {
2433
2368
  ]
2434
2369
  }
2435
2370
  };
2436
- function resolveToken() {
2437
- const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
2438
- if (fromEnv) return fromEnv;
2439
- const creds = readValidCredentials();
2440
- return creds?.token ? String(creds.token).trim() : "";
2441
- }
2442
- function safeApiBase(raw) {
2443
- let parsed;
2444
- try {
2445
- parsed = new URL(raw);
2446
- } catch {
2447
- return {
2448
- ok: false,
2449
- message: `Invalid platform URL: ${raw}`
2450
- };
2451
- }
2452
- const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
2453
- if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
2454
- ok: false,
2455
- message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
2456
- };
2457
- return {
2458
- ok: true,
2459
- base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
2460
- };
2461
- }
2462
- function fail(name, message) {
2463
- return JSON.stringify({
2371
+ async function publish_handler(args) {
2372
+ const token = resolveToken();
2373
+ if (!token) return JSON.stringify({
2464
2374
  ok: false,
2465
2375
  error: {
2466
- name,
2467
- message
2376
+ name: "PublishAuthError",
2377
+ message: "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
2468
2378
  }
2469
2379
  });
2380
+ const result = await publish({
2381
+ ttlHours: args.ttlHours,
2382
+ buildSha: args.buildSha,
2383
+ api: args.api,
2384
+ token
2385
+ });
2386
+ return result.ok ? JSON.stringify(result.data) : JSON.stringify(result);
2470
2387
  }
2471
- async function publish_handler(args) {
2472
- const token = resolveToken();
2473
- if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
2474
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
2475
- if (!apiCheck.ok) return fail("PublishConfigError", apiCheck.message);
2476
- const url = `${apiCheck.base}/api/cli/publish`;
2477
- const body = {};
2478
- if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
2479
- if (args.buildSha) body.buildSha = args.buildSha;
2480
- let res;
2481
- try {
2482
- res = await fetch(url, {
2483
- method: "POST",
2484
- headers: {
2485
- authorization: `Bearer ${token}`,
2486
- "content-type": "application/json"
2487
- },
2488
- body: JSON.stringify(body)
2489
- });
2490
- } catch (err) {
2491
- return fail("PublishNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2492
- }
2493
- const text = await res.text();
2494
- let data;
2495
- try {
2496
- data = JSON.parse(text);
2497
- } catch {
2498
- data = {
2499
- message: text
2500
- };
2501
- }
2502
- if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
2503
- return JSON.stringify(data);
2504
- }
2505
- const release_promote_DEFAULT_API = "https://www.extension.dev";
2388
+ const DEFAULT_API = "https://www.extension.dev";
2506
2389
  const release_promote_schema = {
2507
2390
  name: "extension_release_promote",
2508
2391
  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).",
@@ -2547,7 +2430,7 @@ const release_promote_schema = {
2547
2430
  ]
2548
2431
  }
2549
2432
  };
2550
- function release_promote_fail(name, message) {
2433
+ function fail(name, message) {
2551
2434
  return JSON.stringify({
2552
2435
  ok: false,
2553
2436
  error: {
@@ -2558,12 +2441,12 @@ function release_promote_fail(name, message) {
2558
2441
  }
2559
2442
  async function release_promote_handler(args) {
2560
2443
  const token = resolveToken();
2561
- if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
2444
+ if (!token) return fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
2562
2445
  const buildId = String(args.buildId || "").trim();
2563
2446
  const channel = String(args.channel || "").trim();
2564
- if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
2565
- const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
2566
- if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
2447
+ if (!buildId || !channel) return fail("ReleaseInputError", "buildId and channel are required.");
2448
+ const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
2449
+ if (!apiCheck.ok) return fail("ReleaseConfigError", apiCheck.message);
2567
2450
  const url = `${apiCheck.base}/api/cli/release/promote`;
2568
2451
  const body = {
2569
2452
  buildId,
@@ -2584,7 +2467,7 @@ async function release_promote_handler(args) {
2584
2467
  body: JSON.stringify(body)
2585
2468
  });
2586
2469
  } catch (err) {
2587
- return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2470
+ return fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2588
2471
  }
2589
2472
  const text = await res.text();
2590
2473
  let data;
@@ -2595,7 +2478,7 @@ async function release_promote_handler(args) {
2595
2478
  message: text
2596
2479
  };
2597
2480
  }
2598
- if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
2481
+ if (!res.ok) return fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
2599
2482
  return JSON.stringify(data);
2600
2483
  }
2601
2484
  const wait_schema = {
@@ -2893,138 +2776,6 @@ async function add_feature_handler(args) {
2893
2776
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
2894
2777
  });
2895
2778
  }
2896
- const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
2897
- const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
2898
- const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2899
- const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
2900
- async function startDeviceCode(args) {
2901
- const doFetch = args.fetchImpl ?? fetch;
2902
- const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
2903
- method: "POST",
2904
- headers: {
2905
- accept: "application/json",
2906
- "content-type": "application/json"
2907
- },
2908
- body: JSON.stringify({
2909
- client_id: args.clientId,
2910
- scope: args.scope || "read:user"
2911
- })
2912
- });
2913
- const data = await res.json().catch(()=>({}));
2914
- if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
2915
- return {
2916
- deviceCode: String(data.device_code || ""),
2917
- userCode: String(data.user_code || ""),
2918
- verificationUri: String(data.verification_uri || "https://github.com/login/device"),
2919
- interval: Number(data.interval || 5),
2920
- expiresIn: Number(data.expires_in || 900)
2921
- };
2922
- }
2923
- async function pollForToken(args) {
2924
- const doFetch = args.fetchImpl ?? fetch;
2925
- const sleep = args.sleepImpl ?? defaultSleep;
2926
- let intervalMs = 1000 * Math.max(1, args.interval);
2927
- const deadline = Date.now() + Math.max(0, args.budgetMs);
2928
- while(Date.now() < deadline){
2929
- await sleep(intervalMs);
2930
- const res = await doFetch(GITHUB_TOKEN_URL, {
2931
- method: "POST",
2932
- headers: {
2933
- accept: "application/json",
2934
- "content-type": "application/json"
2935
- },
2936
- body: JSON.stringify({
2937
- client_id: args.clientId,
2938
- device_code: args.deviceCode,
2939
- grant_type: DEVICE_GRANT_TYPE
2940
- })
2941
- });
2942
- const data = await res.json().catch(()=>({}));
2943
- const token = String(data.access_token || "").trim();
2944
- if (token) return {
2945
- ok: true,
2946
- githubToken: token
2947
- };
2948
- const error = String(data.error || "");
2949
- if ("authorization_pending" === error) continue;
2950
- if ("slow_down" === error) {
2951
- intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
2952
- continue;
2953
- }
2954
- if ("expired_token" === error) return {
2955
- ok: false,
2956
- reason: "expired"
2957
- };
2958
- if ("access_denied" === error) return {
2959
- ok: false,
2960
- reason: "denied"
2961
- };
2962
- }
2963
- return {
2964
- ok: false,
2965
- reason: "pending"
2966
- };
2967
- }
2968
- const login_flow_DEFAULT_API = "https://www.extension.dev";
2969
- function resolveApiBase(api) {
2970
- return String(api || process.env.EXTENSION_DEV_API_URL || login_flow_DEFAULT_API).replace(/\/+$/, "");
2971
- }
2972
- async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
2973
- const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
2974
- if (override) return {
2975
- clientId: override,
2976
- scope: "read:user"
2977
- };
2978
- const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
2979
- headers: {
2980
- accept: "application/json"
2981
- }
2982
- });
2983
- if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
2984
- const data = await res.json().catch(()=>({}));
2985
- const clientId = String(data.githubClientId || "").trim();
2986
- if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
2987
- return {
2988
- clientId,
2989
- scope: String(data.scope || "read:user")
2990
- };
2991
- }
2992
- async function exchangeAndPersist(args) {
2993
- const doFetch = args.fetchImpl ?? fetch;
2994
- const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
2995
- method: "POST",
2996
- headers: {
2997
- "content-type": "application/json",
2998
- accept: "application/json"
2999
- },
3000
- body: JSON.stringify({
3001
- githubToken: args.githubToken,
3002
- project: args.project
3003
- })
3004
- });
3005
- const text = await res.text();
3006
- let data;
3007
- try {
3008
- data = JSON.parse(text);
3009
- } catch {
3010
- data = {
3011
- message: text
3012
- };
3013
- }
3014
- if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
3015
- const token = String(data.token || "").trim();
3016
- if (!token) throw new Error("Login exchange returned no token.");
3017
- const creds = {
3018
- version: 1,
3019
- token,
3020
- workspaceSlug: String(data.workspaceSlug || ""),
3021
- projectSlug: String(data.projectSlug || ""),
3022
- expiresAt: Number(data.expiresAt || 0),
3023
- api: args.apiBase
3024
- };
3025
- writeCredentials(creds);
3026
- return creds;
3027
- }
3028
2779
  const login_schema = {
3029
2780
  name: "extension_login",
3030
2781
  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.",
@@ -24,23 +24,6 @@ export declare const schema: {
24
24
  required: string[];
25
25
  };
26
26
  };
27
- /** Resolve the access token: EXTENSION_DEV_TOKEN env first, then the login creds file. */
28
- export declare function resolveToken(): string;
29
- /**
30
- * Validate the platform base URL BEFORE we attach a bearer token to a request to
31
- * it. SECURITY: the `api` arg / EXTENSION_DEV_API_URL is operator-supplied, but a
32
- * hostile value (e.g. via prompt-injection in the client) could redirect the
33
- * token to an attacker. The access token must never leave over plaintext or go to
34
- * an arbitrary scheme, so we require https -- allowing http only for localhost
35
- * dev. Returns the normalized base (no trailing slash) or an error message.
36
- */
37
- export declare function safeApiBase(raw: string): {
38
- ok: true;
39
- base: string;
40
- } | {
41
- ok: false;
42
- message: string;
43
- };
44
27
  export declare function handler(args: {
45
28
  projectPath: string;
46
29
  ttlHours?: number;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "4.0.8",
5
- "description": "MCP server that lets AI agents build, run, inspect, and publish browser extensions. 26 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, and Firefox.",
4
+ "version": "4.1.1",
5
+ "description": "MCP server that lets AI agents build, run, inspect, and publish browser extensions. 27 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, and Firefox.",
6
6
  "license": "MIT",
7
7
  "author": {
8
8
  "name": "Cezar Augusto",
@@ -62,6 +62,7 @@
62
62
  "extension.dev"
63
63
  ],
64
64
  "dependencies": {
65
+ "@extension.dev/core": "^0.2.0",
65
66
  "@modelcontextprotocol/sdk": "^1.12.1",
66
67
  "browser-extension-manifest-fields": "^2.2.9",
67
68
  "cross-spawn": "^7.0.6",
@@ -1,24 +0,0 @@
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;
@@ -1,40 +0,0 @@
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 {};
@@ -1,25 +0,0 @@
1
- import { type StoredCredentials } from "./credentials";
2
- type FetchImpl = typeof fetch;
3
- export declare function resolveApiBase(api?: string): string;
4
- export interface LoginConfig {
5
- clientId: string;
6
- scope: string;
7
- }
8
- /**
9
- * Resolve the GitHub OAuth client id the device flow authenticates against.
10
- * EXTENSION_DEV_GITHUB_CLIENT_ID overrides (useful when pointing at a self-
11
- * hosted platform); otherwise it comes from the platform's public config.
12
- */
13
- export declare function fetchLoginConfig(apiBase: string, fetchImpl?: FetchImpl): Promise<LoginConfig>;
14
- /**
15
- * Trade a verified GitHub user token for a project-scoped access token and
16
- * write it to the local credentials file. Returns the stored credentials
17
- * (token included for the caller's in-memory use; never logged).
18
- */
19
- export declare function exchangeAndPersist(args: {
20
- apiBase: string;
21
- githubToken: string;
22
- project: string;
23
- fetchImpl?: FetchImpl;
24
- }): Promise<StoredCredentials>;
25
- export {};