@extension.dev/mcp 3.17.0-canary.1779910200.ddcd9eb → 3.17.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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 3.17.0
4
+
5
+ First stable release on npm. The registry previously carried only canary
6
+ builds (3.17.0-canary.*), so `npx @extension.dev/mcp` resolved a canary;
7
+ this release graduates that line to stable and becomes `latest`.
8
+
9
+ - 26 tools across scaffolding, build/dev/preview, live inspection (CDP +
10
+ agent bridge), act tools (eval/storage/reload/open), browser management,
11
+ and platform auth/publish.
12
+ - Claude Code integration assets (CLAUDE.md, slash commands, rules) and the
13
+ @extension.dev/skill pairing.
14
+ - MIT license shipped; repository moved to extensiondev/mcp.
15
+
1
16
  # @extension.dev/mcp — Changelog
2
17
 
3
18
  ## Unreleased — agent-bridge tools
@@ -34,3 +49,36 @@ New tools (22 total):
34
49
  Internal: `lib/act` (CLI shell-out helper), `lib/exec.runExtensionCli` (capture),
35
50
  `lib/cdp.getClosedShadowRoots`. Test infra aligned to the workspace vitest
36
51
  catalog.
52
+
53
+ ## Unreleased — login (auth tools)
54
+
55
+ Adds the missing `login` flow so `extension_publish` no longer requires the user
56
+ to mint and export `EXTENSION_DEV_TOKEN` by hand. Auth stays auth-AWARE: the
57
+ token lives in a local credentials file, never in the MCP process state or logs.
58
+
59
+ New tools (25 total):
60
+
61
+ - **`extension_login`** — GitHub **device-code** flow (no local server; works
62
+ headless). Two-phase: call with `project` (`<workspace>/<project>`) to get a
63
+ code + URL, call again with the returned `deviceCode` to finish. On success it
64
+ writes a project-scoped token to the credentials file. Never returns the token.
65
+ - **`extension_whoami`** — report the stored workspace/project and token expiry
66
+ without revealing the token.
67
+ - **`extension_logout`** — delete the local credentials file.
68
+
69
+ Token resolution for publish is now `EXTENSION_DEV_TOKEN` env **>** the
70
+ credentials file (expired file tokens are ignored).
71
+
72
+ Credentials file (versioned, `0600`): `$XDG_CONFIG_HOME/extension-dev/auth.json`
73
+ (or `~/.config/...`; `%APPDATA%\extension-dev\auth.json` on Windows).
74
+
75
+ Platform endpoints this depends on (in `apps/www.extension.dev`):
76
+
77
+ - `GET /api/cli/login/config` — public GitHub OAuth client id + scope.
78
+ - `POST /api/cli/login/exchange` — trades a GitHub **user** token for a
79
+ project-scoped access token after checking workspace membership. Modeled on
80
+ `/api/oidc/exchange`; tokens are recorded so they stay revocable.
81
+
82
+ > ⚠️ **Ops:** the device flow requires **device flow enabled** on the GitHub
83
+ > OAuth App behind `WWW_GITHUB_OAUTH_CLIENT_ID`. Until then, `extension_login`
84
+ > can't complete and users fall back to a dashboard-minted `EXTENSION_DEV_TOKEN`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Extension Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,27 +1,46 @@
1
+ [npm-version-image]: https://img.shields.io/npm/v/%40extension.dev%2Fmcp
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
5
+
6
+ [![Version][npm-version-image]][npm-version-url] [![workflow][action-image]][action-url]
7
+
1
8
  # @extension.dev/mcp
2
9
 
3
- MCP server for building, debugging, and publishing cross-browser extensions. Exposes 15 tools that let AI assistants scaffold, develop, build, inspect, and debug extensions using the [extension.dev](https://extension.dev) platform and template catalog.
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:
11
+
12
+ ```bash
13
+ claude mcp add extension-dev npx @extension.dev/mcp
14
+ ```
15
+
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).
4
17
 
5
18
  ## What's in this package
6
19
 
7
20
  ```
8
21
  @extension.dev/mcp
9
- src/ MCP server source (15 tools across 3 tiers)
22
+ src/ MCP server source (26 tools)
10
23
  claude/ Claude Code integration (CLAUDE.md, slash commands, rules)
11
24
  bin/ CLI entrypoint
12
25
  ```
13
26
 
14
- **MCP server** programmatic bridge between AI assistants and the extension.dev platform. Imports from published npm packages:
27
+ **MCP server**: programmatic bridge between AI assistants and the extension.dev platform. Imports from published npm packages:
15
28
 
16
- - `extension-create` project scaffolding
17
- - `extension-develop` build/dev/preview
18
- - `extension-install` managed browser binaries
29
+ - `extension-create`: project scaffolding
30
+ - `extension-develop`: build/dev/preview
31
+ - `extension-install`: managed browser binaries
19
32
 
20
- **Claude Code integration** drop-in instructions, slash commands, and rules for Claude Code:
33
+ **Claude Code integration**: drop-in instructions, slash commands, and rules for Claude Code:
21
34
 
22
- - `claude/CLAUDE.md` project-level instructions for any extension project
23
- - `claude/commands/` slash commands (`/extension`, `/extension-add`, `/extension-debug`, `/extension-publish`)
24
- - `claude/rules/` rules for extension development, cross-browser compat, and MCP tools
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
38
+
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
+ ```
25
44
 
26
45
  Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension` CLI since they require the full browser launcher infrastructure.
27
46
 
@@ -73,11 +92,22 @@ cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/c
73
92
  | 2 | `extension_manifest_validate` | native | Cross-browser manifest validation |
74
93
  | 2 | `extension_inspect` | native | Build output analysis |
75
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 |
76
98
  | 2 | `extension_wait` | native | Poll ready.json contract |
77
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` |
78
104
  | 3 | `extension_install_browser` | `extensionInstall()` | Install managed browser |
79
105
  | 3 | `extension_list_browsers` | `getManagedBrowsersCacheRoot()` | List managed browsers |
80
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) |
81
111
 
82
112
  ## Development
83
113
 
@@ -96,6 +126,18 @@ NPM_TOKEN=<token> pnpm publish
96
126
 
97
127
  Uses `prepublishOnly` to run tests and compile before publishing.
98
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.
140
+
99
141
  ## License
100
142
 
101
143
  MIT
@@ -1,3 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import {startServer} from '../dist/module.js'
3
- startServer()
2
+ import {startServer, runCli} from '../dist/module.js'
3
+
4
+ const [, , cmd, ...rest] = process.argv
5
+
6
+ if (cmd && ['login', 'logout', 'whoami'].includes(cmd)) {
7
+ runCli(cmd, rest)
8
+ .then((code) => process.exit(code))
9
+ .catch((err) => {
10
+ process.stderr.write(`${err?.message || String(err)}\n`)
11
+ process.exit(1)
12
+ })
13
+ } else {
14
+ // No subcommand: run as the MCP stdio server (the default and primary mode).
15
+ startServer()
16
+ }
package/claude/CLAUDE.md CHANGED
@@ -242,6 +242,14 @@ npm run dev -- --logs info --log-url "example.com"
242
242
  - Use `--wait` flag to check if dev session is ready (outputs ready.json contract)
243
243
  - Use `npm run start` to test production builds (builds first, then launches)
244
244
 
245
+ ### Triggering events without clicking (requires `--allow-control`)
246
+
247
+ - `extension open action` — fire the toolbar action (opens its popup, or replays `chrome.action.onClicked`).
248
+ - `extension open command --name <cmd>` — replay a `chrome.commands.onCommand` keyboard shortcut.
249
+ - `extension_list_extensions` (MCP tool; no CLI verb) — list extensions with a live context in the browser (Chromium, read-only).
250
+
251
+ These replay your captured listeners, so they work on Chrome and Firefox — but carry **no user gesture**, so `activeTab` is not granted (the result reports `gesture: false`, plus a `warning` when the manifest declares `activeTab`). If you need a genuine-gesture click (activeTab granted), use [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp)'s `trigger_extension_action` (Chromium only) alongside this server.
252
+
245
253
  ## Contributing templates to the examples repo
246
254
 
247
255
  If you create a new extension pattern worth sharing:
@@ -24,7 +24,17 @@ Debug the currently running extension dev session. The user said: $ARGUMENTS
24
24
  - Read `dist/extension-js/chrome/ready.json` to get the CDP port
25
25
  - Report the port and suggest Chrome DevTools inspection
26
26
 
27
- 3. **Diagnose common issues**
27
+ 3. **Exercise event handlers (when the bug is in an action / command / shortcut)**
28
+ If the session was started with `--allow-control`, fire the events a user would, without clicking:
29
+ - `extension_open` with `surface: "action"` — triggers the toolbar action (opens its popup, or replays `chrome.action.onClicked`).
30
+ - `extension_open` with `surface: "command"` and `name: "<cmd>"` — replays a `chrome.commands.onCommand` keyboard shortcut.
31
+ - Then re-inspect or read `extension_logs` to see what the handler did.
32
+
33
+ **Caveat:** replay carries no user gesture, so `activeTab` is NOT granted. If the handler depends on `activeTab` (e.g. `chrome.scripting.executeScript` on the active tab, `captureVisibleTab`), the result includes `gesture: false` and a `warning`, and behavior differs from a real click. For a genuine-gesture click (activeTab granted), use chrome-devtools-mcp's `trigger_extension_action` (Chromium only) alongside this server.
34
+
35
+ To see what else is loaded in the browser (Chromium): `extension_list_extensions`.
36
+
37
+ 4. **Diagnose common issues**
28
38
  Based on what you find, check for:
29
39
  - **"It didn't load"**: Check extension root count. If 0, content scripts may not be injecting. Check manifest `content_scripts` matches patterns and the target URL.
30
40
  - **Console errors**: Report top errors. Common ones:
@@ -34,7 +44,7 @@ Debug the currently running extension dev session. The user said: $ARGUMENTS
34
44
  - **Wrong page**: Check if the content script `matches` pattern in manifest covers the target URL
35
45
  - **Shadow DOM empty**: Extension root exists but shadow content is empty — likely a CSS or framework mounting issue
36
46
 
37
- 4. **Suggest fixes** based on the diagnosis
47
+ 5. **Suggest fixes** based on the diagnosis
38
48
 
39
49
  ## Examples
40
50
 
@@ -719,8 +719,15 @@ The `similarTemplates` field lists templates from the catalog with similar surfa
719
719
  | `extension_manifest_validate` | `programs/develop` | `plugin-web-extension` | `templates-meta.json` for similar templates | Extract validation logic |
720
720
  | `extension_inspect` | `programs/develop` | `--source` flag logic | — | Extract into callable API |
721
721
  | `extension_source_inspect` | `programs/extension` | CDP client / RDP transport | Live browser via debugging protocol | Wire to running session |
722
+ | `extension_list_extensions` | MCP `lib/cdp` | `Extensions.getExtensionInfo` (read-only) | Live browser via CDP (Chromium) | MCP tool (no CLI verb) |
722
723
  | `extension_wait` | `programs/extension` | `dev-wait.ts` | `ready.json` contract file | Thin wrapper (exists in CLI) |
723
724
  | `extension_add_feature` | New | `extension_get_template_source` | examples repo patterns | Codegen from examples |
725
+ | **Agent bridge — act / triggers** | | | | |
726
+ | `extension_eval` | `programs/extension` | bridge control channel | Live extension context | Wraps `extension eval` (`--allow-eval`) |
727
+ | `extension_storage` | `programs/extension` | bridge control channel | `chrome.storage` | Wraps `extension storage` |
728
+ | `extension_reload` | `programs/extension` | bridge control channel | Live extension | Wraps `extension reload` |
729
+ | `extension_open` | `programs/extension` | bridge control channel | Surfaces + `action`/`command` replay | Wraps `extension open` |
730
+ | `extension_logs` | `programs/extension` | bridge log/control channel | `logs.ndjson` + live channel | Wraps `extension logs` |
724
731
  | **Tier 3 — Browser management** | | | | |
725
732
  | `extension_install_browser` | `programs/install` | `extensionInstall()` | — | Thin wrapper only |
726
733
  | `extension_list_browsers` | `programs/install` | `getManagedBrowsersCacheRoot()` | — | Thin wrapper only |
package/dist/module.d.ts CHANGED
@@ -1 +1 @@
1
- export { startServer } from "./src/index";
1
+ export { startServer, runCli } from "./src/index";
package/dist/module.js CHANGED
@@ -78,12 +78,30 @@ __webpack_require__.d(list_browsers_namespaceObject, {
78
78
  handler: ()=>list_browsers_handler,
79
79
  schema: ()=>list_browsers_schema
80
80
  });
81
+ var list_extensions_namespaceObject = {};
82
+ __webpack_require__.r(list_extensions_namespaceObject);
83
+ __webpack_require__.d(list_extensions_namespaceObject, {
84
+ handler: ()=>list_extensions_handler,
85
+ schema: ()=>list_extensions_schema
86
+ });
81
87
  var list_templates_namespaceObject = {};
82
88
  __webpack_require__.r(list_templates_namespaceObject);
83
89
  __webpack_require__.d(list_templates_namespaceObject, {
84
90
  handler: ()=>list_templates_handler,
85
91
  schema: ()=>list_templates_schema
86
92
  });
93
+ var login_namespaceObject = {};
94
+ __webpack_require__.r(login_namespaceObject);
95
+ __webpack_require__.d(login_namespaceObject, {
96
+ handler: ()=>login_handler,
97
+ schema: ()=>login_schema
98
+ });
99
+ var logout_namespaceObject = {};
100
+ __webpack_require__.r(logout_namespaceObject);
101
+ __webpack_require__.d(logout_namespaceObject, {
102
+ handler: ()=>logout_handler,
103
+ schema: ()=>logout_schema
104
+ });
87
105
  var logs_namespaceObject = {};
88
106
  __webpack_require__.r(logs_namespaceObject);
89
107
  __webpack_require__.d(logs_namespaceObject, {
@@ -112,6 +130,7 @@ var publish_namespaceObject = {};
112
130
  __webpack_require__.r(publish_namespaceObject);
113
131
  __webpack_require__.d(publish_namespaceObject, {
114
132
  handler: ()=>publish_handler,
133
+ resolveToken: ()=>resolveToken,
115
134
  schema: ()=>publish_schema
116
135
  });
117
136
  var reload_namespaceObject = {};
@@ -144,6 +163,12 @@ __webpack_require__.d(wait_namespaceObject, {
144
163
  handler: ()=>wait_handler,
145
164
  schema: ()=>wait_schema
146
165
  });
166
+ var whoami_namespaceObject = {};
167
+ __webpack_require__.r(whoami_namespaceObject);
168
+ __webpack_require__.d(whoami_namespaceObject, {
169
+ handler: ()=>whoami_handler,
170
+ schema: ()=>whoami_schema
171
+ });
147
172
  const schema = {
148
173
  name: "extension_create",
149
174
  description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
@@ -1546,6 +1571,120 @@ async function source_inspect_handler(args) {
1546
1571
  cdp.disconnect();
1547
1572
  }
1548
1573
  }
1574
+ const list_extensions_schema = {
1575
+ name: "extension_list_extensions",
1576
+ description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). Identity is read read-only via the Extensions domain — other extensions' contexts are never attached to or evaluated in. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
1577
+ inputSchema: {
1578
+ type: "object",
1579
+ properties: {
1580
+ projectPath: {
1581
+ type: "string",
1582
+ description: "Path to the extension project root (must have an active dev session)"
1583
+ },
1584
+ browser: {
1585
+ type: "string",
1586
+ default: "chrome"
1587
+ }
1588
+ },
1589
+ required: [
1590
+ "projectPath"
1591
+ ]
1592
+ }
1593
+ };
1594
+ async function list_extensions_handler(args) {
1595
+ const browser = args.browser ?? "chrome";
1596
+ const isChromium = [
1597
+ "chrome",
1598
+ "edge",
1599
+ "chromium-based"
1600
+ ].includes(browser);
1601
+ if (!isChromium) return JSON.stringify({
1602
+ error: `Listing extensions for ${browser} uses RDP (Remote Debug Protocol). Currently only Chromium CDP is supported.`,
1603
+ hint: "Use --browser=chrome."
1604
+ });
1605
+ const cdpPort = await list_extensions_findCdpPort(args.projectPath, browser);
1606
+ if (!cdpPort) return JSON.stringify({
1607
+ error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
1608
+ hint: "Start a dev session first with extension_dev, then use extension_wait to confirm it is ready."
1609
+ });
1610
+ const cdp = new CDPClient();
1611
+ try {
1612
+ const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
1613
+ await cdp.connect(browserWsUrl);
1614
+ const targets = await cdp.getTargets();
1615
+ const byId = new Map();
1616
+ for (const t of targets){
1617
+ const url = String(t.url ?? "");
1618
+ if (!url.startsWith("chrome-extension://")) continue;
1619
+ const id = url.slice(19).split("/")[0];
1620
+ if (!id) continue;
1621
+ const list = byId.get(id) ?? [];
1622
+ list.push({
1623
+ type: String(t.type ?? ""),
1624
+ url
1625
+ });
1626
+ byId.set(id, list);
1627
+ }
1628
+ const extensions = [];
1629
+ for (const [id, ctxTargets] of byId){
1630
+ const entry = {
1631
+ id,
1632
+ contexts: ctxTargets.map((c)=>({
1633
+ type: c.type,
1634
+ url: c.url
1635
+ })),
1636
+ source: "target-only"
1637
+ };
1638
+ try {
1639
+ const info = await cdp.sendCommand("Extensions.getExtensionInfo", {
1640
+ extensionId: id
1641
+ });
1642
+ if (info?.extensionInfo) {
1643
+ entry.name = info.extensionInfo.name;
1644
+ entry.version = info.extensionInfo.version;
1645
+ entry.source = "extensions-domain";
1646
+ }
1647
+ } catch {}
1648
+ extensions.push(entry);
1649
+ }
1650
+ extensions.sort((a, b)=>(a.name ?? a.id).localeCompare(b.name ?? b.id));
1651
+ return JSON.stringify({
1652
+ cdpPort,
1653
+ browser,
1654
+ count: extensions.length,
1655
+ extensions,
1656
+ note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. Identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
1657
+ });
1658
+ } catch (error) {
1659
+ return JSON.stringify({
1660
+ error: `Failed to list extensions: ${error.message}`
1661
+ });
1662
+ } finally{
1663
+ cdp.disconnect();
1664
+ }
1665
+ }
1666
+ async function list_extensions_findCdpPort(projectPath, browser) {
1667
+ const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
1668
+ try {
1669
+ const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
1670
+ if ("number" == typeof contract.cdpPort) return contract.cdpPort;
1671
+ } catch {}
1672
+ const defaultPort = 9222;
1673
+ return new Promise((resolve)=>{
1674
+ const socket = new node_net.Socket();
1675
+ socket.setTimeout(1000);
1676
+ socket.on("connect", ()=>{
1677
+ socket.destroy();
1678
+ resolve(defaultPort);
1679
+ });
1680
+ socket.on("error", ()=>resolve(null));
1681
+ socket.on("timeout", ()=>{
1682
+ socket.destroy();
1683
+ resolve(null);
1684
+ });
1685
+ socket.connect(defaultPort, "127.0.0.1");
1686
+ });
1687
+ }
1549
1688
  const CONTROL_ENVELOPE_VERSION = 1;
1550
1689
  const CONTROL_WS_PATH = "/extjs-control";
1551
1690
  const DEFAULT_LIMIT = 200;
@@ -2046,7 +2185,7 @@ async function reload_handler(args) {
2046
2185
  }
2047
2186
  const open_schema = {
2048
2187
  name: "extension_open",
2049
- description: "Open an extension surface popup, options, or sidebar in a running session. Requires the dev session to be started with --allow-control. Wraps `extension open`.",
2188
+ description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces. 'action' triggers the toolbar action: opens the action's popup, or (no popup) replays chrome.action.onClicked. 'command' replays a chrome.commands.onCommand keyboard shortcut (pass `name`). NOTE: action/command replay invokes your listener WITHOUT a user gesture, so the gesture-derived activeTab grant does not apply (the result includes gesture:false and a warning when activeTab is declared). Requires --allow-control. Wraps `extension open`.",
2050
2189
  inputSchema: {
2051
2190
  type: "object",
2052
2191
  properties: {
@@ -2059,9 +2198,15 @@ const open_schema = {
2059
2198
  enum: [
2060
2199
  "popup",
2061
2200
  "options",
2062
- "sidebar"
2201
+ "sidebar",
2202
+ "action",
2203
+ "command"
2063
2204
  ],
2064
- description: "Which surface to open"
2205
+ description: "Which surface to open or event to replay. 'action' triggers the toolbar action; 'command' replays a keyboard-shortcut command (requires `name`)."
2206
+ },
2207
+ name: {
2208
+ type: "string",
2209
+ description: "For surface 'command': the chrome.commands name to trigger."
2065
2210
  },
2066
2211
  browser: {
2067
2212
  type: "string",
@@ -2084,6 +2229,7 @@ async function open_handler(args) {
2084
2229
  args.surface,
2085
2230
  args.projectPath
2086
2231
  ];
2232
+ if ("command" === args.surface && args.name) cli.push("--name", args.name);
2087
2233
  if (args.browser) cli.push("--browser", args.browser);
2088
2234
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2089
2235
  return runActVerb(cli, args.projectPath, args.timeout);
@@ -2179,9 +2325,73 @@ async function dom_inspect_handler(args) {
2179
2325
  if (null != args.timeout) cli.push("--timeout", String(args.timeout));
2180
2326
  return runActVerb(cli, args.projectPath, args.timeout);
2181
2327
  }
2328
+ function credentialsPath() {
2329
+ if ("win32" === process.platform) {
2330
+ const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
2331
+ return node_path.join(base, "extension-dev", "auth.json");
2332
+ }
2333
+ const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
2334
+ const base = xdg || node_path.join(node_os.homedir(), ".config");
2335
+ return node_path.join(base, "extension-dev", "auth.json");
2336
+ }
2337
+ function readCredentials() {
2338
+ try {
2339
+ const raw = node_fs.readFileSync(credentialsPath(), "utf8");
2340
+ const data = JSON.parse(raw);
2341
+ if (!data || "object" != typeof data) return null;
2342
+ if (1 !== data.version) return null;
2343
+ const token = String(data.token || "").trim();
2344
+ if (!token) return null;
2345
+ return {
2346
+ version: 1,
2347
+ token,
2348
+ workspaceSlug: String(data.workspaceSlug || ""),
2349
+ projectSlug: String(data.projectSlug || ""),
2350
+ expiresAt: Number(data.expiresAt || 0),
2351
+ api: String(data.api || "")
2352
+ };
2353
+ } catch {
2354
+ return null;
2355
+ }
2356
+ }
2357
+ function writeCredentials(creds) {
2358
+ const file = credentialsPath();
2359
+ node_fs.mkdirSync(node_path.dirname(file), {
2360
+ recursive: true
2361
+ });
2362
+ node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
2363
+ mode: 384
2364
+ });
2365
+ try {
2366
+ node_fs.chmodSync(file, 384);
2367
+ } catch {}
2368
+ return file;
2369
+ }
2370
+ function clearCredentials() {
2371
+ const file = credentialsPath();
2372
+ try {
2373
+ node_fs.unlinkSync(file);
2374
+ return {
2375
+ cleared: true,
2376
+ path: file
2377
+ };
2378
+ } catch {
2379
+ return {
2380
+ cleared: false,
2381
+ path: file
2382
+ };
2383
+ }
2384
+ }
2385
+ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
2386
+ const creds = readCredentials();
2387
+ if (!creds) return null;
2388
+ if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
2389
+ return creds;
2390
+ }
2391
+ const DEFAULT_API = "https://www.extension.dev";
2182
2392
  const publish_schema = {
2183
2393
  name: "extension_publish",
2184
- 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. Wraps `extension publish` (which posts to the platform's CLI publish endpoint). This is the only tool that talks to the hosted platform rather than the local browser.",
2394
+ 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.",
2185
2395
  inputSchema: {
2186
2396
  type: "object",
2187
2397
  properties: {
@@ -2207,32 +2417,54 @@ const publish_schema = {
2207
2417
  ]
2208
2418
  }
2209
2419
  };
2210
- async function publish_handler(args) {
2211
- const cli = [
2212
- "publish",
2213
- args.projectPath,
2214
- "--output",
2215
- "json"
2216
- ];
2217
- if (null != args.ttlHours) cli.push("--ttl", String(args.ttlHours));
2218
- if (args.buildSha) cli.push("--build-sha", args.buildSha);
2219
- if (args.api) cli.push("--api", args.api);
2220
- const { code, stdout, stderr } = await runExtensionCli(cli, {
2221
- cwd: args.projectPath
2222
- });
2223
- const out = stdout.trim();
2224
- if (out) try {
2225
- JSON.parse(out);
2226
- return out;
2227
- } catch {}
2420
+ function resolveToken() {
2421
+ const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
2422
+ if (fromEnv) return fromEnv;
2423
+ const creds = readValidCredentials();
2424
+ return creds?.token ? String(creds.token).trim() : "";
2425
+ }
2426
+ function fail(name, message) {
2228
2427
  return JSON.stringify({
2229
2428
  ok: false,
2230
2429
  error: {
2231
- name: "PublishError",
2232
- message: stderr.trim() || `extension publish exited with code ${code}`
2430
+ name,
2431
+ message
2233
2432
  }
2234
2433
  });
2235
2434
  }
2435
+ async function publish_handler(args) {
2436
+ const token = resolveToken();
2437
+ if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
2438
+ const base = String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
2439
+ const url = `${base}/api/cli/publish`;
2440
+ const body = {};
2441
+ if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
2442
+ if (args.buildSha) body.buildSha = args.buildSha;
2443
+ let res;
2444
+ try {
2445
+ res = await fetch(url, {
2446
+ method: "POST",
2447
+ headers: {
2448
+ authorization: `Bearer ${token}`,
2449
+ "content-type": "application/json"
2450
+ },
2451
+ body: JSON.stringify(body)
2452
+ });
2453
+ } catch (err) {
2454
+ return fail("PublishNetworkError", `Could not reach ${url}: ${err?.message || err}`);
2455
+ }
2456
+ const text = await res.text();
2457
+ let data;
2458
+ try {
2459
+ data = JSON.parse(text);
2460
+ } catch {
2461
+ data = {
2462
+ message: text
2463
+ };
2464
+ }
2465
+ if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
2466
+ return JSON.stringify(data);
2467
+ }
2236
2468
  const wait_schema = {
2237
2469
  name: "extension_wait",
2238
2470
  description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
@@ -2528,6 +2760,303 @@ async function add_feature_handler(args) {
2528
2760
  hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
2529
2761
  });
2530
2762
  }
2763
+ const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
2764
+ const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
2765
+ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2766
+ const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
2767
+ async function startDeviceCode(args) {
2768
+ const doFetch = args.fetchImpl ?? fetch;
2769
+ const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
2770
+ method: "POST",
2771
+ headers: {
2772
+ accept: "application/json",
2773
+ "content-type": "application/json"
2774
+ },
2775
+ body: JSON.stringify({
2776
+ client_id: args.clientId,
2777
+ scope: args.scope || "read:user"
2778
+ })
2779
+ });
2780
+ const data = await res.json().catch(()=>({}));
2781
+ if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
2782
+ return {
2783
+ deviceCode: String(data.device_code || ""),
2784
+ userCode: String(data.user_code || ""),
2785
+ verificationUri: String(data.verification_uri || "https://github.com/login/device"),
2786
+ interval: Number(data.interval || 5),
2787
+ expiresIn: Number(data.expires_in || 900)
2788
+ };
2789
+ }
2790
+ async function pollForToken(args) {
2791
+ const doFetch = args.fetchImpl ?? fetch;
2792
+ const sleep = args.sleepImpl ?? defaultSleep;
2793
+ let intervalMs = 1000 * Math.max(1, args.interval);
2794
+ const deadline = Date.now() + Math.max(0, args.budgetMs);
2795
+ while(Date.now() < deadline){
2796
+ await sleep(intervalMs);
2797
+ const res = await doFetch(GITHUB_TOKEN_URL, {
2798
+ method: "POST",
2799
+ headers: {
2800
+ accept: "application/json",
2801
+ "content-type": "application/json"
2802
+ },
2803
+ body: JSON.stringify({
2804
+ client_id: args.clientId,
2805
+ device_code: args.deviceCode,
2806
+ grant_type: DEVICE_GRANT_TYPE
2807
+ })
2808
+ });
2809
+ const data = await res.json().catch(()=>({}));
2810
+ const token = String(data.access_token || "").trim();
2811
+ if (token) return {
2812
+ ok: true,
2813
+ githubToken: token
2814
+ };
2815
+ const error = String(data.error || "");
2816
+ if ("authorization_pending" === error) continue;
2817
+ if ("slow_down" === error) {
2818
+ intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
2819
+ continue;
2820
+ }
2821
+ if ("expired_token" === error) return {
2822
+ ok: false,
2823
+ reason: "expired"
2824
+ };
2825
+ if ("access_denied" === error) return {
2826
+ ok: false,
2827
+ reason: "denied"
2828
+ };
2829
+ }
2830
+ return {
2831
+ ok: false,
2832
+ reason: "pending"
2833
+ };
2834
+ }
2835
+ const login_flow_DEFAULT_API = "https://www.extension.dev";
2836
+ function resolveApiBase(api) {
2837
+ return String(api || process.env.EXTENSION_DEV_API_URL || login_flow_DEFAULT_API).replace(/\/+$/, "");
2838
+ }
2839
+ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
2840
+ const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
2841
+ if (override) return {
2842
+ clientId: override,
2843
+ scope: "read:user"
2844
+ };
2845
+ const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
2846
+ headers: {
2847
+ accept: "application/json"
2848
+ }
2849
+ });
2850
+ if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
2851
+ const data = await res.json().catch(()=>({}));
2852
+ const clientId = String(data.githubClientId || "").trim();
2853
+ if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
2854
+ return {
2855
+ clientId,
2856
+ scope: String(data.scope || "read:user")
2857
+ };
2858
+ }
2859
+ async function exchangeAndPersist(args) {
2860
+ const doFetch = args.fetchImpl ?? fetch;
2861
+ const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
2862
+ method: "POST",
2863
+ headers: {
2864
+ "content-type": "application/json",
2865
+ accept: "application/json"
2866
+ },
2867
+ body: JSON.stringify({
2868
+ githubToken: args.githubToken,
2869
+ project: args.project
2870
+ })
2871
+ });
2872
+ const text = await res.text();
2873
+ let data;
2874
+ try {
2875
+ data = JSON.parse(text);
2876
+ } catch {
2877
+ data = {
2878
+ message: text
2879
+ };
2880
+ }
2881
+ if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
2882
+ const token = String(data.token || "").trim();
2883
+ if (!token) throw new Error("Login exchange returned no token.");
2884
+ const creds = {
2885
+ version: 1,
2886
+ token,
2887
+ workspaceSlug: String(data.workspaceSlug || ""),
2888
+ projectSlug: String(data.projectSlug || ""),
2889
+ expiresAt: Number(data.expiresAt || 0),
2890
+ api: args.apiBase
2891
+ };
2892
+ writeCredentials(creds);
2893
+ return creds;
2894
+ }
2895
+ const login_schema = {
2896
+ name: "extension_login",
2897
+ 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.",
2898
+ inputSchema: {
2899
+ type: "object",
2900
+ properties: {
2901
+ project: {
2902
+ type: "string",
2903
+ description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
2904
+ },
2905
+ deviceCode: {
2906
+ type: "string",
2907
+ description: "Resume token from a prior call's `deviceCode`; omit on the first call"
2908
+ },
2909
+ api: {
2910
+ type: "string",
2911
+ description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
2912
+ }
2913
+ },
2914
+ required: [
2915
+ "project"
2916
+ ]
2917
+ }
2918
+ };
2919
+ const FIRST_CALL_BUDGET_MS = 8000;
2920
+ const RESUME_BUDGET_MS = 22000;
2921
+ function login_fail(name, message) {
2922
+ return JSON.stringify({
2923
+ ok: false,
2924
+ error: {
2925
+ name,
2926
+ message
2927
+ }
2928
+ });
2929
+ }
2930
+ function success(creds) {
2931
+ return JSON.stringify({
2932
+ ok: true,
2933
+ status: "logged-in",
2934
+ workspaceSlug: creds.workspaceSlug,
2935
+ projectSlug: creds.projectSlug,
2936
+ expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
2937
+ message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token.`
2938
+ });
2939
+ }
2940
+ function login_pending(start) {
2941
+ return JSON.stringify({
2942
+ ok: false,
2943
+ status: "authorization_pending",
2944
+ userCode: start.userCode,
2945
+ verificationUri: start.verificationUri,
2946
+ deviceCode: start.deviceCode,
2947
+ message: `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_login again with this deviceCode (and the same project).`
2948
+ });
2949
+ }
2950
+ async function login_handler(args) {
2951
+ const project = String(args.project || "").trim();
2952
+ if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
2953
+ const apiBase = resolveApiBase(args.api);
2954
+ let config;
2955
+ try {
2956
+ config = await fetchLoginConfig(apiBase);
2957
+ } catch (err) {
2958
+ return login_fail("LoginConfigError", err?.message || "Could not load login config.");
2959
+ }
2960
+ if (args.deviceCode) {
2961
+ const poll = await pollForToken({
2962
+ clientId: config.clientId,
2963
+ deviceCode: String(args.deviceCode),
2964
+ interval: 5,
2965
+ budgetMs: RESUME_BUDGET_MS
2966
+ });
2967
+ if (!poll.ok) {
2968
+ if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
2969
+ if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
2970
+ return login_pending({
2971
+ deviceCode: String(args.deviceCode),
2972
+ userCode: "(see the previous response)",
2973
+ verificationUri: "https://github.com/login/device"
2974
+ });
2975
+ }
2976
+ try {
2977
+ const creds = await exchangeAndPersist({
2978
+ apiBase,
2979
+ githubToken: poll.githubToken,
2980
+ project
2981
+ });
2982
+ return success(creds);
2983
+ } catch (err) {
2984
+ return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
2985
+ }
2986
+ }
2987
+ let start;
2988
+ try {
2989
+ start = await startDeviceCode({
2990
+ clientId: config.clientId,
2991
+ scope: config.scope
2992
+ });
2993
+ } catch (err) {
2994
+ return login_fail("LoginStartError", err?.message || "Could not start device flow.");
2995
+ }
2996
+ const poll = await pollForToken({
2997
+ clientId: config.clientId,
2998
+ deviceCode: start.deviceCode,
2999
+ interval: start.interval,
3000
+ budgetMs: FIRST_CALL_BUDGET_MS
3001
+ });
3002
+ if (poll.ok) try {
3003
+ const creds = await exchangeAndPersist({
3004
+ apiBase,
3005
+ githubToken: poll.githubToken,
3006
+ project
3007
+ });
3008
+ return success(creds);
3009
+ } catch (err) {
3010
+ return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
3011
+ }
3012
+ if (!poll.ok && "denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
3013
+ return login_pending(start);
3014
+ }
3015
+ const whoami_schema = {
3016
+ name: "extension_whoami",
3017
+ description: "Report the locally stored extension.dev login (workspace/project and token expiry) without revealing the token. Returns logged-out status when no credentials are stored.",
3018
+ inputSchema: {
3019
+ type: "object",
3020
+ properties: {}
3021
+ }
3022
+ };
3023
+ async function whoami_handler() {
3024
+ const creds = readCredentials();
3025
+ if (!creds) return JSON.stringify({
3026
+ ok: true,
3027
+ status: "logged-out",
3028
+ message: "No stored credentials. Run extension_login to authenticate."
3029
+ });
3030
+ const now = Math.floor(Date.now() / 1000);
3031
+ const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
3032
+ return JSON.stringify({
3033
+ ok: true,
3034
+ status: expired ? "expired" : "logged-in",
3035
+ workspaceSlug: creds.workspaceSlug,
3036
+ projectSlug: creds.projectSlug,
3037
+ api: creds.api,
3038
+ expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
3039
+ expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
3040
+ expired,
3041
+ message: expired ? "Stored token has expired. Run extension_login to refresh it." : `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`
3042
+ });
3043
+ }
3044
+ const logout_schema = {
3045
+ name: "extension_logout",
3046
+ description: "Delete the locally stored extension.dev credentials. Does not revoke the token server-side (revoke from the dashboard if needed); only removes it from this machine.",
3047
+ inputSchema: {
3048
+ type: "object",
3049
+ properties: {}
3050
+ }
3051
+ };
3052
+ async function logout_handler() {
3053
+ const result = clearCredentials();
3054
+ return JSON.stringify({
3055
+ ok: true,
3056
+ cleared: result.cleared,
3057
+ message: result.cleared ? "Local credentials removed." : "No stored credentials to remove."
3058
+ });
3059
+ }
2531
3060
  const install_browser_schema = {
2532
3061
  name: "extension_install_browser",
2533
3062
  description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed.",
@@ -2867,6 +3396,7 @@ const tools = [
2867
3396
  manifest_validate_namespaceObject,
2868
3397
  inspect_namespaceObject,
2869
3398
  source_inspect_namespaceObject,
3399
+ list_extensions_namespaceObject,
2870
3400
  logs_namespaceObject,
2871
3401
  eval_namespaceObject,
2872
3402
  storage_namespaceObject,
@@ -2876,6 +3406,9 @@ const tools = [
2876
3406
  publish_namespaceObject,
2877
3407
  wait_namespaceObject,
2878
3408
  add_feature_namespaceObject,
3409
+ login_namespaceObject,
3410
+ whoami_namespaceObject,
3411
+ logout_namespaceObject,
2879
3412
  install_browser_namespaceObject,
2880
3413
  list_browsers_namespaceObject,
2881
3414
  detect_browsers_namespaceObject
@@ -2941,4 +3474,60 @@ async function startServer() {
2941
3474
  const transport = new StdioServerTransport();
2942
3475
  await server.connect(transport);
2943
3476
  }
2944
- export { startServer };
3477
+ async function runCli(cmd, args) {
3478
+ const log = (msg)=>process.stderr.write(`${msg}\n`);
3479
+ const flag = (name)=>{
3480
+ const i = args.indexOf(`--${name}`);
3481
+ return i >= 0 ? args[i + 1] : void 0;
3482
+ };
3483
+ if ("whoami" === cmd) {
3484
+ log(await whoami_handler());
3485
+ return 0;
3486
+ }
3487
+ if ("logout" === cmd) {
3488
+ log(await logout_handler());
3489
+ return 0;
3490
+ }
3491
+ if ("login" === cmd) {
3492
+ const project = String(flag("project") || "").trim();
3493
+ if (!/^[^/]+\/[^/]+$/.test(project)) {
3494
+ log("Usage: extension-mcp login --project <workspace>/<project> [--api <url>]");
3495
+ return 1;
3496
+ }
3497
+ const apiBase = resolveApiBase(flag("api"));
3498
+ try {
3499
+ const config = await fetchLoginConfig(apiBase);
3500
+ const start = await startDeviceCode({
3501
+ clientId: config.clientId,
3502
+ scope: config.scope
3503
+ });
3504
+ log("");
3505
+ log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
3506
+ log("");
3507
+ log(" Waiting for authorization...");
3508
+ const poll = await pollForToken({
3509
+ clientId: config.clientId,
3510
+ deviceCode: start.deviceCode,
3511
+ interval: start.interval,
3512
+ budgetMs: 1000 * start.expiresIn
3513
+ });
3514
+ if (!poll.ok) {
3515
+ log("denied" === poll.reason ? "Authorization was denied on GitHub." : "Timed out waiting for authorization. Run login again.");
3516
+ return 1;
3517
+ }
3518
+ const creds = await exchangeAndPersist({
3519
+ apiBase,
3520
+ githubToken: poll.githubToken,
3521
+ project
3522
+ });
3523
+ log(`Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`);
3524
+ return 0;
3525
+ } catch (err) {
3526
+ log(err instanceof Error ? err.message : String(err));
3527
+ return 1;
3528
+ }
3529
+ }
3530
+ log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami.`);
3531
+ return 1;
3532
+ }
3533
+ export { runCli, startServer };
@@ -1 +1,9 @@
1
1
  export declare function startServer(): Promise<void>;
2
+ /**
3
+ * Human-facing subcommands for `extension-mcp <cmd>`. Unlike the MCP tools
4
+ * (which must return promptly and so resume across calls), the bin blocks on
5
+ * the device flow because there is a person watching the terminal. Returns a
6
+ * process exit code. All console output goes to stderr so it never pollutes a
7
+ * machine-readable stdout if a script captures it.
8
+ */
9
+ export declare function runCli(cmd: string, args: string[]): Promise<number>;
@@ -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,25 @@
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 {};
@@ -0,0 +1,22 @@
1
+ export declare const schema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ projectPath: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ browser: {
12
+ type: string;
13
+ default: string;
14
+ };
15
+ };
16
+ required: string[];
17
+ };
18
+ };
19
+ export declare function handler(args: {
20
+ projectPath: string;
21
+ browser?: string;
22
+ }): Promise<string>;
@@ -0,0 +1,27 @@
1
+ export declare const schema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ project: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ deviceCode: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ api: {
16
+ type: string;
17
+ description: string;
18
+ };
19
+ };
20
+ required: string[];
21
+ };
22
+ };
23
+ export declare function handler(args: {
24
+ project: string;
25
+ deviceCode?: string;
26
+ api?: string;
27
+ }): Promise<string>;
@@ -0,0 +1,9 @@
1
+ export declare const schema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {};
7
+ };
8
+ };
9
+ export declare function handler(): Promise<string>;
@@ -14,6 +14,10 @@ export declare const schema: {
14
14
  enum: string[];
15
15
  description: string;
16
16
  };
17
+ name: {
18
+ type: string;
19
+ description: string;
20
+ };
17
21
  browser: {
18
22
  type: string;
19
23
  default: string;
@@ -28,4 +32,5 @@ export declare const schema: {
28
32
  };
29
33
  export declare function handler(args: ActArgs & {
30
34
  surface: string;
35
+ name?: string;
31
36
  }): Promise<string>;
@@ -24,6 +24,8 @@ 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;
27
29
  export declare function handler(args: {
28
30
  projectPath: string;
29
31
  ttlHours?: number;
@@ -0,0 +1,9 @@
1
+ export declare const schema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {};
7
+ };
8
+ };
9
+ export declare function handler(): Promise<string>;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@extension.dev/mcp",
3
3
  "type": "module",
4
- "version": "3.17.0-canary.1779910200.ddcd9eb",
5
- "description": "MCP server for building, debugging, and publishing cross-browser extensions. Gives AI assistants programmatic access to the extension.dev platform and template catalog.",
4
+ "version": "3.17.0",
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.",
6
6
  "license": "MIT",
7
7
  "author": {
8
8
  "name": "Cezar Augusto",
@@ -11,15 +11,16 @@
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/extensiondev/extension-dev.git"
14
+ "url": "git+https://github.com/extensiondev/mcp.git"
15
15
  },
16
16
  "bugs": {
17
- "url": "https://github.com/extensiondev/extension-dev/issues"
17
+ "url": "https://github.com/extensiondev/mcp/issues"
18
18
  },
19
- "homepage": "https://github.com/extensiondev/extension-dev#readme",
19
+ "homepage": "https://github.com/extensiondev/mcp#readme",
20
20
  "engines": {
21
21
  "node": ">=20.18"
22
22
  },
23
+ "packageManager": "pnpm@10.19.0",
23
24
  "exports": {
24
25
  ".": {
25
26
  "types": "./dist/module.d.ts",
@@ -39,6 +40,15 @@
39
40
  "CHANGELOG.md",
40
41
  "LICENSE"
41
42
  ],
43
+ "scripts": {
44
+ "clean": "rm -rf dist",
45
+ "watch": "rslib build --watch",
46
+ "compile": "rslib build",
47
+ "build": "pnpm run compile",
48
+ "start": "node bin/extension-mcp.js",
49
+ "test": "vitest run",
50
+ "prepublishOnly": "pnpm run test && pnpm run compile"
51
+ },
42
52
  "publishConfig": {
43
53
  "access": "public",
44
54
  "registry": "https://registry.npmjs.org"
@@ -46,15 +56,20 @@
46
56
  "keywords": [
47
57
  "mcp",
48
58
  "model-context-protocol",
49
- "browser-extension",
50
- "extension.dev",
51
- "browser-extension-development",
52
- "extension-dev",
59
+ "ai-agent",
60
+ "agents",
53
61
  "claude",
54
62
  "claude-code",
55
- "ai-tools",
63
+ "claude-desktop",
64
+ "cursor",
65
+ "browser-extension",
66
+ "chrome-extension",
67
+ "firefox-addon",
68
+ "manifest-v3",
56
69
  "webextension",
57
- "cross-browser"
70
+ "cross-browser",
71
+ "developer-tools",
72
+ "extension.dev"
58
73
  ],
59
74
  "dependencies": {
60
75
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -68,14 +83,6 @@
68
83
  "@types/node": "^25.2.0",
69
84
  "@types/ws": "^8.18.1",
70
85
  "typescript": "6.0.2",
71
- "vitest": "3.2.2"
72
- },
73
- "scripts": {
74
- "clean": "rm -rf dist",
75
- "watch": "rslib build --watch",
76
- "compile": "rslib build",
77
- "build": "pnpm run compile",
78
- "start": "node bin/extension-mcp.js",
79
- "test": "vitest run"
86
+ "vitest": "^4.1.3"
80
87
  }
81
- }
88
+ }
@@ -1,2 +0,0 @@
1
- declare const _default: import("@rslib/core").RslibConfig;
2
- export default _default;
@@ -1,3 +0,0 @@
1
- import type { UserConfig } from "vitest/config";
2
- declare const config: UserConfig;
3
- export default config;