@extension.dev/mcp 10.10.9 → 10.10.11

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.
Files changed (77) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +85 -1
  4. package/README.md +10 -10
  5. package/claude/ARCHITECTURE.md +13 -13
  6. package/claude/CLAUDE.md +11 -10
  7. package/claude/README.md +4 -4
  8. package/claude/commands/extension-debug.md +1 -1
  9. package/claude/commands/extension-publish.md +1 -1
  10. package/claude/commands/extension.md +1 -1
  11. package/claude/rules/cross-browser.md +4 -3
  12. package/claude/rules/mcp-tools.md +446 -1009
  13. package/dist/module.js +4621 -1501
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/lib/allowance.d.ts +1 -1
  16. package/dist/src/lib/artifacts-api.d.ts +1 -0
  17. package/dist/src/lib/boot-verdict.d.ts +5 -0
  18. package/dist/src/lib/bridge-tabs.d.ts +3 -1
  19. package/dist/src/lib/browser-family.d.ts +1 -0
  20. package/dist/src/lib/carrier-registry.d.ts +4 -0
  21. package/dist/src/lib/carrier.d.ts +10 -1
  22. package/dist/src/lib/cdp-connection.d.ts +2 -1
  23. package/dist/src/lib/cdp-devtools.d.ts +5 -0
  24. package/dist/src/lib/cdp-extension-page.d.ts +26 -2
  25. package/dist/src/lib/cdp-page-scripts.d.ts +2 -2
  26. package/dist/src/lib/cdp-port.d.ts +1 -1
  27. package/dist/src/lib/cdp.d.ts +13 -1
  28. package/dist/src/lib/common-schema.d.ts +1 -1
  29. package/dist/src/lib/create-answer.d.ts +20 -0
  30. package/dist/src/lib/credential-source.d.ts +25 -0
  31. package/dist/src/lib/credentials.d.ts +23 -0
  32. package/dist/src/lib/docs-tools.d.ts +9 -0
  33. package/dist/src/lib/engine-manifest-view.d.ts +1 -0
  34. package/dist/src/lib/envelope.d.ts +1 -1
  35. package/dist/src/lib/exec.d.ts +3 -0
  36. package/dist/src/lib/extension-identity.d.ts +7 -0
  37. package/dist/src/lib/first-build.d.ts +15 -0
  38. package/dist/src/lib/guest-load-oracle.d.ts +2 -1
  39. package/dist/src/lib/launch-flags.d.ts +2 -2
  40. package/dist/src/lib/match-patterns.d.ts +1 -1
  41. package/dist/src/lib/node-engine.d.ts +11 -0
  42. package/dist/src/lib/platform-hold.d.ts +1 -0
  43. package/dist/src/lib/process-identity.d.ts +5 -0
  44. package/dist/src/lib/process-manager.d.ts +5 -1
  45. package/dist/src/lib/project-manifest.d.ts +1 -0
  46. package/dist/src/lib/promote-outcome.d.ts +29 -0
  47. package/dist/src/lib/publish.d.ts +2 -0
  48. package/dist/src/lib/registry-access.d.ts +1 -0
  49. package/dist/src/lib/registry.d.ts +6 -1
  50. package/dist/src/lib/safari-automation.d.ts +2 -0
  51. package/dist/src/lib/session-browser.d.ts +5 -0
  52. package/dist/src/lib/session-paths.d.ts +1 -0
  53. package/dist/src/lib/store-review.d.ts +14 -2
  54. package/dist/src/lib/submit-outcome.d.ts +14 -0
  55. package/dist/src/lib/templates-cache.d.ts +13 -0
  56. package/dist/src/lib/types.d.ts +2 -1
  57. package/dist/src/lib/vendor/chrome-theme/chrome-theme-resolve.d.ts +1 -0
  58. package/dist/src/tools/assert.d.ts +4 -1
  59. package/dist/src/tools/create.d.ts +1 -0
  60. package/dist/src/tools/dev.d.ts +2 -2
  61. package/dist/src/tools/dom-snapshot.d.ts +1 -1
  62. package/dist/src/tools/eval.d.ts +1 -1
  63. package/dist/src/tools/inspect-gecko.d.ts +2 -1
  64. package/dist/src/tools/logs-filter.d.ts +6 -2
  65. package/dist/src/tools/open.d.ts +4 -1
  66. package/dist/src/tools/project-create.d.ts +1 -0
  67. package/dist/src/tools/reload.d.ts +1 -1
  68. package/dist/src/tools/start.d.ts +2 -2
  69. package/dist/src/tools/stop.d.ts +10 -0
  70. package/dist/src/tools/storage.d.ts +1 -6
  71. package/dist/src/tools/store-status.d.ts +4 -0
  72. package/extensions/live-preview/chromium/action/index.css +1 -1
  73. package/extensions/live-preview/chromium/action/index.js +1 -1
  74. package/extensions/live-preview/chromium/background/service_worker.js +98 -4
  75. package/extensions/live-preview/chromium/manifest.json +1 -1
  76. package/package.json +3 -2
  77. package/server.json +3 -3
@@ -1,1012 +1,449 @@
1
- # extension.dev MCP Tool Specification
1
+ # MCP tools
2
2
 
3
- Design document for `@extension.dev/mcp`, an MCP server that exposes extension.dev capabilities as tools for Claude Code, Claude Desktop, and any MCP-compatible client.
3
+ Generated from the schemas the server registers (`src/index.ts`); do not edit by hand. Regenerate with `pnpm docs:tools`. A test fails when this file and the schemas disagree.
4
4
 
5
- ## Why this matters
5
+ 32 tools.
6
+
7
+ ## extension_add_feature
8
+
9
+ Plan a new feature surface for an existing extension. This returns step-by-step instructions, the manifest additions to make, and reference templates from the extension.dev catalog. It modifies no files: apply the returned plan yourself.
10
+
11
+ | input | type | required | default | description |
12
+ | --- | --- | --- | --- | --- |
13
+ | `projectPath` | string | yes | | Extension project root |
14
+ | `feature` | "sidebar" \| "popup" \| "options" \| "content-script" \| "background" \| "newtab" \| "devtools" | yes | | Feature surface to add |
15
+ | `framework` | "react" \| "vue" \| "svelte" \| "preact" \| "vanilla" | no | `"react"` | |
16
+
17
+ ## extension_analyze
18
+
19
+ Analyze a BUILT extension on disk: file sizes, declared entry points, permissions, bundle composition, and store-readiness checks. This is static only: it reads dist/<browser> from the filesystem and never touches a browser, so build first with extension_build. Use extension_inspect for a running extension's live DOM and console.
20
+
21
+ | input | type | required | default | description |
22
+ | --- | --- | --- | --- | --- |
23
+ | `projectPath` | string | yes | | Extension project root |
24
+ | `browser` | string | no | `"chrome"` | Browser build to analyze |
25
+ | `format` | "summary" \| "tree" \| "json" | no | `"summary"` | |
26
+
27
+ ## extension_assert
28
+
29
+ Run a test stage against a live dev session: state expectations and read one verdict for each, instead of reading a blob and hand-rolling the judgement. Every expectation comes back pass, fail or inconclusive, where inconclusive means this platform cannot cover the question today and the verdict says what would settle it. An inconclusive check is never a pass. Start the session with extension_dev; use extension_inspect or extension_logs when you want the raw reading instead of a verdict.
30
+
31
+ | input | type | required | default | description |
32
+ | --- | --- | --- | --- | --- |
33
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
34
+ | `expect` | array of object | yes | | One object per expectation, each { assert: <check id>, ...args }. background-worker-booted: no args. surface-rendered: surface (popup, options, sidebar, newtab, history, bookmarks), optional selector and minNodes. content-script-injected: url. storage-key-present: key, optional area (default local), equals, context. console-errors-empty: optional context (array), since (seq cursor), ignore (substrings). |
35
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
36
+ | `timeout` | number | no | | Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port. |
37
+
38
+ ## extension_auth
39
+
40
+ Sign this machine in to extension.dev, report that login, or clear it. Pass action:'status' (the default) to name the workspace and project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone, and does not change with the current working directory or whichever project folder you are in. Status also asks the platform's whoami endpoint whether that credential actually resolves there: the answer rides value.server.verdict as confirmed, refused or unavailable (when the server could not be reached), with status logged-in for the first and last and refused-by-server for the second, so a local file claiming a login the server refused is never reported as logged in. Pass action:'login' for a two-phase flow: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days, server-enforced, so CI must re-mint before expiry on the console's project settings, Access tokens page. Pass action:'logout' to delete the local credentials only (with `project`, just that project's login); the token stays valid server-side until it is revoked at the URL the response returns. Several logins live side by side on one machine, one per workspace/project, and status lists them all under `logins`. To sign in to several existing projects of one workspace at once, pass `projects` instead of `project`: one approval, one stored token per project.
41
+
42
+ | input | type | required | default | description |
43
+ | --- | --- | --- | --- | --- |
44
+ | `action` | "status" \| "login" \| "logout" | no | `"status"` | |
45
+ | `project` | string | no | | login: target project as '<workspace>/<project>'; the token is scoped to it. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. Create one at extension.dev/new if none exists yet. Logins to several projects are all kept, the latest is the default; token-scoped tools take `project` to pick another. logout: remove only this project's login (omitted, every stored login goes). |
46
+ | `projects` | array of string | no | | login: sign in to several existing projects of one workspace with one approval, instead of one approval each. 1 to 20 names as '<workspace>/<project>', all in the same workspace, each by its exact slug (lowercase letters and digits joined by single dashes, at most 48 characters), none twice. Pass it instead of `project`, never beside it. The approval page lists every name; one missing project refuses the whole list and mints nothing. Resume with the returned `deviceCode` and the same list. Every token is stored as that project's own login and all expire within 7 days, so the same call renews them together. |
47
+ | `deviceCode` | string | no | | login: resume token from the prior call's `deviceCode`; omit on the first call. |
48
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
49
+
50
+ ## extension_browsers
51
+
52
+ Find, install and remove the browsers Extension.js tooling can launch. Pass action:'detect' (the default) to scan both system-installed and managed browsers, and report each one's binary path, version, engine and debugger support. Pass action:'list' for the managed cache this tool downloads into, with sizes on disk. Pass action:'install' to download a managed binary: several hundred MB in one blocking call, so allow a generous client timeout. Pass action:'uninstall' to remove managed binaries; it never touches a system install.
53
+
54
+ | input | type | required | default | description |
55
+ | --- | --- | --- | --- | --- |
56
+ | `action` | "detect" \| "list" \| "install" \| "uninstall" | no | `"detect"` | |
57
+ | `browsers` | array of "chrome" \| "chromium" \| "edge" \| "brave" \| "opera" \| "vivaldi" \| "yandex" \| "firefox" \| "waterfox" \| "librewolf" \| "zen" \| "floorp" \| "safari" | no | | detect: limit the scan to these. Omit to check all. |
58
+ | `browser` | "chrome" \| "chromium" \| "edge" \| "firefox" | no | | install/uninstall: which managed binary. Required for install. |
59
+ | `all` | boolean | no | `false` | uninstall: remove every managed binary. |
60
+
61
+ ## extension_build
62
+
63
+ Build a browser extension for production. The output lands in dist/<browser>/. Pass zip:true to also package a .zip for store submission. With browser:'safari' the build converts the extension into a macOS app through Xcode, and bundleId sets the identifier it ships under. The build refuses a manifest with build-blocking errors unless you pass skipValidation:true, because such a manifest yields a broken bundle the bundler itself never flags.
64
+
65
+ | input | type | required | default | description |
66
+ | --- | --- | --- | --- | --- |
67
+ | `projectPath` | string | yes | | Extension project root |
68
+ | `browser` | "chrome" \| "chromium" \| "edge" \| "brave" \| "opera" \| "vivaldi" \| "yandex" \| "firefox" \| "waterfox" \| "librewolf" \| "zen" \| "floorp" \| "safari" \| "chromium-based" \| "gecko-based" \| "firefox-based" \| "webkit-based" | no | `"chrome"` | |
69
+ | `zip` | boolean | no | `false` | Create a .zip file for store distribution |
70
+ | `zipSource` | boolean | no | `false` | Include source code zip (required by some stores) |
71
+ | `zipFilename` | string | no | | Custom .zip file name (defaults to name and version) |
72
+ | `polyfill` | boolean | no | `false` | Apply cross-browser polyfill |
73
+ | `silent` | boolean | no | `false` | Suppress build output |
74
+ | `mode` | "development" \| "production" \| "none" | no | `"production"` | Bundler mode override (also sets NODE_ENV) |
75
+ | `skipValidation` | boolean | no | `false` | Build even when extension_manifest_validate reports build-blocking errors. The build normally refuses: the engine itself stops only on missing scripts, icons, DNR rule files, default_locale and manifest_version, and ships a bundle over the rest. |
76
+ | `appName` | string | no | | Safari targets only: name of the generated macOS app, which also names the Xcode scheme and the .app on disk. Defaults to the manifest name. |
77
+ | `bundleId` | string | no | | Safari targets only: a reverse-DNS bundle identifier you own, such as com.acme.readinglist. Without one the app is packaged under a generated dev.extensionjs.* identifier derived from the app name, which two projects with the same name share, and the first team to register it takes it. |
78
+ | `macOsOnly` | boolean | no | `true` | Safari targets only: generate a macOS-only Xcode project. Pass false for a universal project that also targets iOS and iPadOS, which is what you want if the extension ships on iPhone or iPad. |
79
+ | `forceRegenerate` | boolean | no | `false` | Safari targets only: regenerate the Xcode project even when the engine considers it up to date. Use it when an earlier packaging run left the project broken. |
80
+
81
+ ## extension_create
82
+
83
+ Create a browser extension project from a template in the extension.dev catalog. Call extension_templates first to see what is available. The scaffolder may initialize a git repository in the new project (with a first commit), and it also writes store metadata and a .gitignore of its own. Read the result's defaultsApplied block for the decisions this tool can read back: parent directory, template, package manager, target browser and whether a git repository was initialized by this call.
84
+
85
+ | input | type | required | default | description |
86
+ | --- | --- | --- | --- | --- |
87
+ | `projectName` | string | yes | | Name of the extension project (used as directory name). Alias: name. |
88
+ | `parentDir` | string | no | | Directory to create the project inside. Defaults to the MCP server process cwd, NOT the caller's cwd, so pass it whenever you care where the project lands. Aliases: parent, into. |
89
+ | `template` | string | no | `"typescript"` | Template slug from the extension.dev catalog (e.g. 'react', 'ai-claude', 'content-vue'). extension_templates discovers them. |
90
+ | `install` | boolean | no | `true` | Install dependencies after creation |
91
+
92
+ ## extension_dev
93
+
94
+ Run the extension while you edit it: dev build, hot module replacement, and a browser with the extension loaded. Reach for this first when the ask is "run my extension". ONLY this tool unlocks the control channel that extension_storage, extension_reload, extension_open and extension_dom_snapshot need (allowControl:true) and the eval channel that extension_eval needs (allowEval:true, which implies allowControl, so you never need to pass both). Use extension_start instead to run the production build in a browser. The result carries the process info that extension_wait and extension_inspect need.
95
+
96
+ | input | type | required | default | description |
97
+ | --- | --- | --- | --- | --- |
98
+ | `projectPath` | string | yes | | Extension project root |
99
+ | `browser` | "chrome" \| "chromium" \| "edge" \| "brave" \| "opera" \| "vivaldi" \| "yandex" \| "firefox" \| "waterfox" \| "librewolf" \| "zen" \| "floorp" \| "safari" \| "chromium-based" \| "gecko-based" \| "firefox-based" \| "webkit-based" | no | `"chrome"` | |
100
+ | `port` | number | no | | Dev server port (0 for auto-assign) |
101
+ | `noBrowser` | boolean | no | `false` | Start the dev server without launching a browser |
102
+ | `polyfill` | boolean | no | `true` | Apply cross-browser polyfill |
103
+ | `profile` | string | no | | Profile path, or "false" to reuse the real user profile. Omit for a throwaway one. |
104
+ | `startingUrl` | string | no | | URL the browser opens on launch |
105
+ | `chromiumBinary` | string | no | | Custom Chromium-based binary to launch; `browser` still names the target family the engine builds for (chromium-based when none is given). |
106
+ | `geckoBinary` | string | no | | Custom Gecko/Firefox binary to launch; `browser` still names the target family the engine builds for (gecko-based when none is given). |
107
+ | `host` | string | no | | Bind host, default 127.0.0.1. Use 0.0.0.0 in Docker or devcontainers. |
108
+ | `publicHost` | string | no | | Host the browser dials for HMR and reload when it differs from the bind host |
109
+ | `extensions` | array of string | no | | Extra extension paths or store URLs to load alongside the project |
110
+ | `replace` | boolean | no | `false` | Stop the live session for this projectPath first, reported as replacedSession. Without it a second call is refused rather than forking: two sessions fight over one profile and the newer browser dies on the lock. |
111
+ | `allowControl` | boolean | no | `false` | Enable the agent-bridge control channel that extension_storage/reload/open/dom_snapshot need |
112
+ | `allowEval` | boolean | no | `false` | Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so you never need to pass both. |
113
+ | `carrier` | boolean | no | `false` | Load the bundled Live Preview carrier beside your extension (Chromium only) so allowlisted pages (preview.extension.dev, localhost) can pair with the session and stream its real-lane chrome.* trace. Written into the auto-loaded ./extensions folder, gitignored, and removed on extension_stop or extension_build: never part of a release. |
114
+
115
+ ## extension_docs_search
116
+
117
+ Find pages in the Extension.js and extension.dev docs by keyword, each with a short excerpt. Use it before answering from memory on anything version-specific: a CLI flag, a manifest field across browsers, a store submission rule. Free and needs no login.
118
+
119
+ | input | type | required | default | description |
120
+ | --- | --- | --- | --- | --- |
121
+ | `query` | string | yes | | What to look up, in a few words. |
122
+ | `limit` | number | no | `5` | How many pages to return, 1 to 8. |
123
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
124
+
125
+ ## extension_doctor
126
+
127
+ Diagnose a dev session end to end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. This returns one {check, status, detail, remediation?} per leg, in dependency order. Read a 'skip' as blocked, not as a pass: it names the check that blocked it. A session started without allowControl comes back ok:true with status 'read-only', not as an error: its control channel is off by choice. Run this first when any act tool (storage, reload, eval, open) errors unexpectedly. Call it with no projectPath for a pre-flight environment check (node, the Extension.js CLI, the template cache) before any project exists.
128
+
129
+ | input | type | required | default | description |
130
+ | --- | --- | --- | --- | --- |
131
+ | `projectPath` | string | no | | Path to the extension project root. Omit for a pre-flight environment check with no project. |
132
+ | `browser` | string | no | | Browser session to diagnose. Defaults to the active dev session's browser for this project. |
133
+
134
+ ## extension_dom_snapshot
135
+
136
+ Take a shallow structured DOM snapshot of one chosen surface through the agent bridge (localhost only; the snapshot itself needs no CDP, but listTargets and `tabUrl` resolution ask the browser directly and need the session's debug port: CDP page targets on Chromium, RDP tab descriptors on Firefox): element counts, extension roots, open shadow roots, optional byte-capped HTML, and optional recent console lines. This is the SURFACE PICKER: the only tool that reads an open extension surface by name (`context`: popup, options, sidebar, devtools) or an override page, the only one that takes a numeric chrome.tabs id, and the only one that enumerates what is open (listTargets for CDP targetIds and RDP tab actors, listTabs for numeric tab ids). An ambiguous `tabUrl` returns the candidates instead of guessing. It does not pierce closed shadow roots, run selector probes, or navigate: use extension_inspect for those, and for a deep read of an already-open web page. Start the session with allowControl:true (extension_dev).
137
+
138
+ | input | type | required | default | description |
139
+ | --- | --- | --- | --- | --- |
140
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
141
+ | `tab` | number | no | | Numeric chrome.tabs id, only to disambiguate when several tabs match. With neither `tab` nor `url`, content/page target the active tab. |
142
+ | `url` | string | no | | content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`. |
143
+ | `tabUrl` | string | no | | Target the tab whose URL contains this substring (case-insensitive; titles checked only when no url matches). Resolved against the live browser first: exactly one match proceeds, zero or several return the candidates instead of a guess. Alternative to `url`. |
144
+ | `listTargets` | boolean | no | `false` | Enumerate live page targets and return, ignoring the other args. The discovery path for `tabUrl`. Chromium: {targetId,url,title,type}. Firefox: RDP tab descriptors {actor,url,title,type}. Neither id is a numeric chrome.tabs id; for those use listTabs. |
145
+ | `listTabs` | boolean | no | `false` | Enumerate open tabs as {tabId,url,title} and return, ignoring the other args. Use when you need a numeric tab id. |
146
+ | `context` | "content" \| "page" \| "popup" \| "options" \| "sidebar" \| "devtools" \| "newtab" \| "history" \| "bookmarks" | no | `"content"` | content/page targets `url`, else the active tab; the rest must already be OPEN |
147
+ | `include` | array of "summary" \| "html" | no | `["summary"]` | What to include; html is byte-capped |
148
+ | `maxBytes` | number | no | `262144` | |
149
+ | `withConsole` | number \| boolean | no | | Also include recent console lines. A number is how many; true means 50. |
150
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
151
+ | `timeout` | number | no | | Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port. |
152
+
153
+ ## extension_eval
154
+
155
+ Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token; without that token every route of this tool, the debug port included, answers eval-disabled. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab; pass context:'background' to evaluate in the service worker, which on Chromium goes over the debug port. Debug-port evaluates run with a user gesture, so gesture-gated APIs (permissions.request, sidePanel.open) can succeed here and still fail when the extension's own code calls them. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. On Firefox a document whose content security policy forbids eval (the extension's own pages, or a site's) is evaluated over the debugger protocol instead, which takes one expression; a page inside the extension that is no declared surface (pages/*) is reached the same way by context:'page' and its moz-extension:// url once a tab shows it. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.
156
+
157
+ | input | type | required | default | description |
158
+ | --- | --- | --- | --- | --- |
159
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
160
+ | `expression` | string | yes | | JavaScript expression to evaluate in the target context |
161
+ | `context` | "background" \| "popup" \| "options" \| "sidebar" \| "devtools" \| "newtab" \| "history" \| "bookmarks" \| "content" \| "page" | no | | Where to evaluate. Default background, except Chromium MV3 sessions default to page (the active tab). |
162
+ | `url` | string | no | | content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`. |
163
+ | `tab` | number | no | | Numeric chrome.tabs id, only to disambiguate when several tabs match. |
164
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
165
+ | `timeout` | number | no | | Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port. |
166
+
167
+ ## extension_inspect
168
+
169
+ Inspect a running extension deeply over the browser's debugger protocol: full HTML (open shadow roots of #extension-root and [data-extension-root] hosts inlined; other shadow roots are not crossed by html, dom_snapshot or probe), DOM structure, content-script injection, console messages, and CSS selector queries through `probe`. This is the ONLY tool that pierces closed shadow roots (deepDom), runs selector probes, and navigates a tab to `url` before reading it. It reads a web or override page and picks the first inspectable target, or the first whose url contains `url`; it cannot address an extension surface by name and takes no chrome.tabs id. Use extension_dom_snapshot to choose which tab or which open surface (popup, options, sidebar, devtools) to read, or to enumerate what is open. Use extension_analyze for a built extension's files and sizes on disk. Chromium rides the Chrome DevTools Protocol and needs the session's debug port, not allowControl. Firefox is fully paired: summary, meta, html, dom_snapshot, extension_roots and probes ride the agent bridge and need allowEval:true, console rides the RDP watcher replay on engine 4.0.15 and later, and deepDom needs an MV2 session with host permissions for the target url, because the Firefox MV3 background CSP blocks bridge evals. This requires an active dev or start session.
170
+
171
+ | input | type | required | default | description |
172
+ | --- | --- | --- | --- | --- |
173
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
174
+ | `url` | string | no | | URL to inspect; the tab is navigated there first |
175
+ | `probe` | array of string | no | | CSS selectors to query; returns counts and samples for each |
176
+ | `include` | array of "html" \| "summary" \| "meta" \| "dom_snapshot" \| "console" \| "extension_roots" | no | `["summary","meta","console"]` | What to include |
177
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
178
+ | `maxBytes` | number | no | `262144` | Truncate HTML output at this byte count (0 = unlimited) |
179
+ | `deepDom` | boolean | no | `false` | Pierce CLOSED shadow roots (open roots of the extension-root hosts are already inlined in html; other open roots are not read). Chromium: CDP DOM pierce. Firefox: a content-script walk via tabs.executeScript (MV2 only, needs host permissions for the target url); the answer says whether that context could see closed roots at all. |
180
+
181
+ ## extension_list_extensions
182
+
183
+ List the extensions in the running dev browser: id, name, version, and, on Chromium, live contexts. This session's own extension carries ownExtension:true, with name and version from the ready contract even when the browser exposes no identity. Chromium rides the Chrome DevTools Protocol, so an entry needs at least one live context, and a dormant MV3 service worker may be absent until it wakes. Firefox rides the RDP root actor (listAddons, engine 4.0.15 and later), so entries are installed add-ons regardless of contexts, are marked temporarilyInstalled where relevant, and carry no contexts. Other extensions' contexts are never attached to or evaluated in. This requires an active dev or start session.
184
+
185
+ | input | type | required | default | description |
186
+ | --- | --- | --- | --- | --- |
187
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
188
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
189
+
190
+ ## extension_logs
191
+
192
+ Read or stream logs from every context of a running dev session (service worker, content scripts, popup, options, sidebar, devtools, pages) in one ordered timeline. This reads the same agent-bridge plane as the `extension logs` CLI: a one-shot returns the most recent matching lines from logs.ndjson, and follow:true connects to the live control channel, receives the broker's replay of its recent ring (up to 5,000 events, counted in value.replayed) and then the frames that arrive during followMs (value.live). This requires an active extension_dev session.
193
+
194
+ | input | type | required | default | description |
195
+ | --- | --- | --- | --- | --- |
196
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
197
+ | `browser` | string | no | | Which dist/extension-js/<browser>/ to read. Defaults to this project's live session, else chrome. |
198
+ | `level` | "off" \| "error" \| "warn" \| "info" \| "debug" \| "trace" \| "all" | no | `"all"` | Minimum severity; a level includes everything more severe. |
199
+ | `context` | array of "background" \| "content" \| "sidebar" \| "popup" \| "options" \| "devtools" \| "newtab" \| "history" \| "bookmarks" | no | | Restrict to these contexts. Omit for all. |
200
+ | `signalsOnly` | boolean | no | `false` | Only structured dx.signal diagnostics (code/status/remediation), no plain console lines. |
201
+ | `since` | number | no | | Only events with seq greater than this; the cursor for polling forward. |
202
+ | `url` | string | no | | Only events whose url/hostname matches (glob or substring), e.g. https://shop.example/*. |
203
+ | `tab` | number | no | | Only events from this tab id. |
204
+ | `follow` | boolean | no | `false` | Collect from the live control channel for a bounded window instead of reading the file. |
205
+ | `followMs` | number | no | `4000` | How long to collect live frames when follow=true (clamped 500–15000ms). |
206
+ | `limit` | number | no | `200` | How many of the most recent events to return. |
207
+
208
+ ## extension_manifest_validate
209
+
210
+ Validate a manifest.json across browsers. This reports missing fields, invalid permissions, dangling file references, and cross-browser compatibility issues. Read buildBlocking for the errors that make extension_build refuse.
211
+
212
+ | input | type | required | default | description |
213
+ | --- | --- | --- | --- | --- |
214
+ | `manifestPath` | string | no | | Path to manifest.json. Or pass projectPath and the manifest is located for you. |
215
+ | `projectPath` | string | no | | Path to the extension project root; manifest.json is resolved from it (root or src/). Accepted in place of manifestPath. |
216
+ | `browsers` | array of string | no | `["chrome","firefox","edge"]` | Browsers to validate against |
217
+ | `browser` | string | no | | Single browser to validate against; alias for browsers:[browser] to match the other tools. |
218
+
219
+ ## extension_open
220
+
221
+ Open an extension surface, or replay an event, in a running session. Pass surface:'popup', 'options' or 'sidebar' to open a UI surface, or 'newtab', 'history' or 'bookmarks' to open the matching chrome_url_overrides page in a tab (always a tab, resolved by the server, never sent to the engine). On Chromium, when Chrome refuses the sidebar for lack of a user gesture, the server opens the real panel through a synthetic click on the extension's own page and says so in warnings; if that fails too it renders the sidebar document as a tab. Pass surface:'devtools' to open the browser's DevTools on a tab (the one `url` matches, else the first web page) and show the extension's panel there, picked by `panel` title when there are several: Chromium only, over CDP Target.openDevTools, headed or headless; the result names the panel document's url, which extension_eval reads with context 'page' and that url (the panel is no tab, so the tab-based readers do not reach it). Pass surface:'action' to trigger the toolbar action, which opens its popup or replays chrome.action.onClicked when there is none. Pass surface:'command' with `name` to replay a chrome.commands.onCommand shortcut. Note that action and command replay invoke your listener without a user gesture, so the gesture-derived activeTab grant does not apply; the engine's own frame is returned as is. Start the session with allowControl:true (extension_dev).
222
+
223
+ | input | type | required | default | description |
224
+ | --- | --- | --- | --- | --- |
225
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
226
+ | `surface` | "popup" \| "options" \| "sidebar" \| "devtools" \| "newtab" \| "history" \| "bookmarks" \| "action" \| "command" | no | | Which surface to open or event to replay. |
227
+ | `name` | string | no | | For surface 'command': the chrome.commands name to trigger. |
228
+ | `panel` | string | no | | For surface 'devtools': the title the extension gave chrome.devtools.panels.create, when it registers more than one panel. Omitted, the extension's first panel is shown. |
229
+ | `waitMs` | number | no | | For surface 'devtools': how long to wait for the panel to register after DevTools opens (default 15000, up to 120000). Extensions that create their panel on a page event need longer, or `reload`. |
230
+ | `reload` | boolean | no | `false` | For surface 'devtools': reload the inspected tab once DevTools is open, for extensions that create their panel only when the page reports to them on a load that starts with DevTools open (Preact Devtools). Discards the page state under test. |
231
+ | `url` | string | no | | Navigate a real tab here instead of opening a surface, in a NEW tab unless `tab` names one (a blank or new-tab page is reused). An absolute url opens as given; a path with no scheme, such as pages/options.html, is resolved against the extension's own origin. Use for content-script test pages, or a surface as a page. |
232
+ | `tab` | number | no | | With `url`: navigate this chrome.tabs id in place instead of opening a new tab (rides the engine's navigate verb, so the session needs allowControl: true). Without it an existing page is never taken over. |
233
+ | `asTab` | boolean | no | `false` | popup/options/sidebar: render the surface's document in a real tab instead of a popup window. This is how you inspect a surface HEADLESSLY, and it is applied automatically when a headless session refuses to open one. Same page and APIs, but no popup sizing and window.close() closes the tab. |
234
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
235
+ | `timeout` | number | no | | Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port. |
236
+
237
+ ## extension_preview_web
238
+
239
+ Preview an in-progress extension in the web emulator, with no real browser. This builds the project (unless build:false) and previews dist/<browser>. Pass share:true unless you are working inside the extension.dev monorepo: it uploads the build and returns a link anyone can open, with no install, sign-in or dev server, and it is the only lane that works from an npm install of this server. Sharing also serves the build as a zip, so it hands over the built code; read the share property before using it. The default lane instead returns a deep link over the dev-only preview://build scheme, which resolves only against a preview.extension.dev dev server on this machine, so it is for people developing extension.dev itself. Call extension_shares to list and revoke every link shared this way, so one never vanishes with this response.
240
+
241
+ | input | type | required | default | description |
242
+ | --- | --- | --- | --- | --- |
243
+ | `projectPath` | string | yes | | Extension project root |
244
+ | `project` | string | no | | Which stored login to use, as '<workspace>/<project>' (or a project slug that matches one login), when extension_auth has signed in to more than one project on this machine. Omitted, the most recent login is used, after EXTENSION_DEV_TOKEN when that is set. Named, it outranks EXTENSION_DEV_TOKEN. extension_auth (action: status) lists the stored logins. |
245
+ | `browser` | "chrome" \| "chromium" \| "edge" \| "brave" \| "opera" \| "vivaldi" \| "yandex" \| "firefox" \| "waterfox" \| "librewolf" \| "zen" \| "floorp" \| "safari" | no | `"chrome"` | Which dist/<browser> output to preview. The emulator renders it as mocked Chrome either way. |
246
+ | `build` | boolean | no | `true` | Build first. false previews the existing dist/<browser> as-is. |
247
+ | `distPath` | string | no | | Preview this built directory instead of dist/<browser> under projectPath. Implies build:false. |
248
+ | `hostUrl` | string | no | | Origin of the running preview.extension.dev dev server (default http://localhost:3110). |
249
+ | `probe` | boolean | no | `true` | Fetch the surface's dev middleware first to confirm the artifact loads on the local host. With share:true it also checks the shared link the way a browser would, following the zip's redirects and asserting the final response allows the preview origin, and reports that as share.browserLoadable. |
250
+ | `open` | boolean | no | `false` | Also open the deep link in a running session's browser, in a focus-safe background tab. Needs a live extension_dev/extension_start session. |
251
+ | `openIn` | "chrome" \| "chromium" \| "edge" \| "brave" \| "opera" \| "vivaldi" \| "yandex" \| "firefox" \| "waterfox" \| "librewolf" \| "zen" \| "floorp" \| "safari" | no | | Which session's browser to open it in. Defaults to `browser`. |
252
+ | `share` | boolean | no | `false` | Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes for anyone: no install, sign-in or dev server. Uploading is metered against your plan's allowance on extension.dev; left false, the result's share property says what the local deepLink needs, what share:true spends, and the exact call to get a shareable link. It also serves the build as a zip (share.zipUrl), so sharing hands over the code. Needs a token scoped to an extension.dev project (extension_auth or EXTENSION_DEV_TOKEN); without one you get a login hint and the local preview still succeeds. Live until share.expiresAt; DELETE share.revokeUrl to kill it sooner. Revocation is permanent, and re-sharing an unchanged build returns the same link unless it was revoked, so each share is also appended to the project's gitignored .extension.dev/shared-previews.json. |
253
+
254
+ ## extension_project_create
255
+
256
+ Create an extension.dev project for an extension that does not have one yet, without opening the console. Use it right after extension_create and extension_build, once the extension's source is pushed to a GitHub repository, and BEFORE extension_auth: extension_auth can only log in to a project that already exists, and this tool is what brings that project into existence. Ask for nothing but the project slug and the repo; the platform finds the GitHub App installation on the approving account itself, and if there is none it returns a connect link to open. Two-phase, like login: the first call returns a code and a URL where a signed-in member of the workspace approves creating exactly this project; call again with the returned deviceCode to finish. The approval mints a provisioning grant that lives minutes, can only create the one named project, and is never stored on this machine. On success the platform creates the project and its mirror repository, and dispatches the first build when it can: the answer says in `firstBuild` whether one was dispatched and, when none was, why (no commits, no build workflow, a spent build allowance, a paused dispatch). Then run extension_auth (action: login) against the new project, and extension_publish to share it. To create several projects in one workspace under one approval, pass `projects` instead of `project` and `repo`: the approval page lists every name, each project is created by its own request, and each one's 7-day token is stored as that project's login, so no extension_auth call is needed afterwards. A list takes a few calls to finish: while projects remain the answer is status 'creating' with the same deviceCode to call again, and the grant is held in this server's memory only. One approval creates at most 10 projects, the cap the platform states in its login config, because it creates at most 10 per hour for one approving account; the next 10 can start in a new call once that limit allows. A longer list is refused before any approval is asked for, never split silently, and so is any list on a platform that does not advertise batch onboarding.
257
+
258
+ | input | type | required | default | description |
259
+ | --- | --- | --- | --- | --- |
260
+ | `project` | string | no | | Target project as '<workspace>/<project>'. The workspace is the GitHub login of the approving user for personal workspaces; the project slug is the new project's name and must not exist yet. |
261
+ | `repo` | string | no | | Source GitHub repository as '<owner>/<repo>'. The extension's code must be pushed there, and the owner must be the same GitHub account that approves the device code. |
262
+ | `installationId` | string | no | | Optional override. Leave it out: the platform finds the extension.dev GitHub App installation on the approving account itself. Pass it only when an operator needs to name one explicitly, and it must still be an installation on that account or the platform refuses it. |
263
+ | `displayName` | string | no | | Human name for the project. Defaults to the project slug. |
264
+ | `description` | string | no | | Short project description. Defaults to a generic sentence naming the repo. |
265
+ | `installCommand` | string | no | `"npm install"` | Dependency install command the build runs first. |
266
+ | `buildCommand` | string | no | `"npm run build"` | Build command producing the extension bundle. |
267
+ | `outputDirectory` | string | no | `"dist/chrome"` | Directory the build writes the loadable extension into. With more than one browser, `<browser>` in the path becomes each browser's name (Extension.js writes `dist/<browser>`), and when it is left out every browser defaults to `dist/<browser>`. |
268
+ | `browsers` | array of "chrome" \| "edge" \| "firefox" | no | `["chrome"]` | Browsers the platform builds, each enabled with the same install and build command and its own output directory. Pass every browser the extension targets, for example ["chrome", "edge", "firefox"], so the project needs no console visit to go cross-browser. |
269
+ | `outputDirectories` | object | no | | Per-browser output directory overrides, for example {"edge": "build/manifestv3"}. A browser named here wins over `outputDirectory`. |
270
+ | `projects` | array of object | no | | Create several projects in one workspace under one approval, instead of `project` and `repo`. 1 to 10 entries (the platform's cap per approval; never more than 20), each { project: '<workspace>/<project>', repo: '<owner>/<repo>' }, all in the same workspace, each project named by its exact slug (lowercase letters and digits joined by single dashes, at most 48 characters), none twice and none existing yet. An entry may also carry displayName, description, installCommand, buildCommand, outputDirectory, browsers and outputDirectories for that project alone; the same inputs at the top level are the shared default for every entry that leaves them out. The answer carries one row per project: created and logged in, created with no token (run a batch extension_auth login), refused with the platform's code, or not attempted. A refusal on one project never hides the others. |
271
+ | `deviceCode` | string | no | | Resume token from the prior call's `deviceCode`; omit on the first call. A batch returns the same deviceCode until every listed project has an answer. |
272
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
273
+
274
+ ## extension_publish
275
+
276
+ Publish the project your stored token is scoped to (extension_auth, or EXTENSION_DEV_TOKEN) to extension.dev, and return its shareable URL. This is what "deploy" or "ship" an extension usually means; extension_submit is the separate store-review path. The target is the token's project: there is no projectPath, and no local file is uploaded. With several logins stored, pass `project` ('<workspace>/<project>') to pick which one; it outranks EXTENSION_DEV_TOKEN. For a public project the URL is the canonical public page and ttlHours does not apply. For a private one it is a fresh time-limited share link (?share=) whose lifetime is ttlHours.
277
+
278
+ | input | type | required | default | description |
279
+ | --- | --- | --- | --- | --- |
280
+ | `project` | string | no | | Which stored login to use, as '<workspace>/<project>' (or a project slug that matches one login), when extension_auth has signed in to more than one project on this machine. Omitted, the most recent login is used, after EXTENSION_DEV_TOKEN when that is set. Named, it outranks EXTENSION_DEV_TOKEN. extension_auth (action: status) lists the stored logins. |
281
+ | `ttlHours` | number | no | | Private-project share-link lifetime in hours, 1-168 (default 24). Ignored for public projects. |
282
+ | `buildSha` | string | no | | Pin the URL to a build sha (7-40 hex chars). The platform rejects a sha missing from a readable build index; when its index cannot be read it echoes the sha back and a pin on a failed build is accepted, so value.buildSha is the platform's claim, not a verified build. extension_release_status lists the builds it knows. |
283
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
284
+
285
+ ## extension_release_promote
286
+
287
+ Promote a built extension to a release channel (stable, preview, beta, …) on extension.dev, headless. This WRITES: it is the only verb that changes what a channel points at. It is auth-gated by your stored login (extension_auth) or a release token in EXTENSION_DEV_TOKEN, minted and revoked under project settings, Access tokens. Tokens live at most 7 days, so CI must re-mint before expiry. The project comes from the token; with several logins stored, `project` picks which one. Call extension_release_status to find a valid buildId. The status is 'promoted' only when the platform says every asked browser's release was dispatched and the channel pointer moved; 'promoted-partially' lists in its warnings what did not happen (a browser whose dispatch failed, a channel pointer that was not moved) and must not be repeated whole; 'promote-unconfirmed' means the platform's answer did not carry the result, so read extension_release_status before promoting again. Cutting a version-bump PR is not available headlessly, because it writes to your source repo and needs an interactive login.
288
+
289
+ | input | type | required | default | description |
290
+ | --- | --- | --- | --- | --- |
291
+ | `project` | string | no | | Which stored login to use, as '<workspace>/<project>' (or a project slug that matches one login), when extension_auth has signed in to more than one project on this machine. Omitted, the most recent login is used, after EXTENSION_DEV_TOKEN when that is set. Named, it outranks EXTENSION_DEV_TOKEN. extension_auth (action: status) lists the stored logins. |
292
+ | `buildId` | string | yes | | Build commit SHA to promote (a 7-char short SHA is fine) |
293
+ | `channel` | string | yes | | Target release channel, e.g. stable, preview, beta |
294
+ | `sourceChannel` | string | no | | Channel to promote from (optional; inferred otherwise) |
295
+ | `browsers` | array of string | no | | Browsers to release. Optional: when omitted the platform reads the build's browsers from its index, and falls back to chrome alone when that index cannot be read, so pass them to be sure. |
296
+ | `version` | string | no | | Version label for the release (optional) |
297
+ | `releaseNotes` | string | no | | Release notes markdown (optional) |
298
+ | `approvalId` | string | no | | The approval handle returned by a prior approval-required response. Promoting changes what a public channel serves and is not reversible in place, so when the platform's approval gate is on this needs a human approval: call once without this to get an approval id and URL, have a human approve at extension.dev, then call again with the same id. |
299
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
300
+
301
+ ## extension_release_status
302
+
303
+ Read where a project stands on extension.dev, from the public registry (registry.extension.land). This is read-only: it dispatches nothing and promotes nothing. Pass include:'releases' for the release channels (channel to promoted build sha), recent builds, and a public build-page URL for each, which is how you find a valid sha for extension_release_promote, extension_submit or extension_publish. Pass include:'stores' for the per-store picture after an extension_submit (chrome, firefox, edge, safari): configured or not, the last credential health check, the last recorded submission, and the latest review status, read from stores/health.json, stores/status.json and stores/submissions.json. Both are included by default. This defaults to the logged-in project (extension_auth); pass workspace and project to read another. Private projects work when your stored login covers them. Registry state can lag the store dashboards by up to a polling interval.
304
+
305
+ | input | type | required | default | description |
306
+ | --- | --- | --- | --- | --- |
307
+ | `include` | array of "releases" \| "stores" | no | `["releases","stores"]` | Which sections to read. Both by default. |
308
+ | `workspace` | string | no | | Workspace slug override (default: the stored login's). |
309
+ | `project` | string | no | | Project slug override (default: the stored login's). |
310
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
311
+
312
+ ## extension_reload
313
+
314
+ Reload a running extension's background context, or a tab. Start the session with allowControl:true (extension_dev).
315
+
316
+ | input | type | required | default | description |
317
+ | --- | --- | --- | --- | --- |
318
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
319
+ | `context` | "background" \| "content" \| "page" | no | `"background"` | |
320
+ | `tab` | number | no | | For content/page: a specific tab id |
321
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
322
+ | `timeout` | number | no | | Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port. |
323
+
324
+ ## extension_shares
325
+
326
+ List and revoke the public preview links this token has shared, which is what extension_preview_web share:true hands out. Pass action:'list' (the default) for every artifact the logged-in project owns, with its artifactId, name, version, live or dead state, createdAt, expiresAt, revokedAt, size, previewUrl, zipUrl and revokeUrl, so a link whose response you lost is findable again. Each row carries owner and sharedBy as the platform returned them. Read attribution.ownership for who may revoke a share: 'project' means the workspace holds it and any member can pull it back, 'personal' means one person holds it alone, 'unknown' means no owner was disclosed. Read attribution.credit as credit only, never access; it names the publisher, and reads 'CLI token <id>' or 'not recorded' when no person can be named. Pass action:'revoke' with an artifactId, or with any URL of the share, to kill one permanently. Pass projectPath to reconcile against the project's own append-only .extension.dev/shared-previews.json, which is read and never rewritten: a share made on another machine shows as remoteOnly, a record with no live artifact as localOnly. That record is append-only, so localOnly is counted by distinct artifactId and a build re-shared unchanged is one share, not two; server.count and server.matched are share counts, while server.scanned counts records the platform read and is never a share count. This needs the same token as sharing (extension_auth or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint.
327
+
328
+ | input | type | required | default | description |
329
+ | --- | --- | --- | --- | --- |
330
+ | `project` | string | no | | Which stored login to use, as '<workspace>/<project>' (or a project slug that matches one login), when extension_auth has signed in to more than one project on this machine. Omitted, the most recent login is used, after EXTENSION_DEV_TOKEN when that is set. Named, it outranks EXTENSION_DEV_TOKEN. extension_auth (action: status) lists the stored logins. |
331
+ | `action` | "list" \| "revoke" | no | `"list"` | list reads every share this token owns; revoke permanently kills one and cannot be undone. |
332
+ | `artifactId` | string | no | | Which share to revoke (the gen_... id from a share response or from action:"list"). Required for revoke unless url is given. |
333
+ | `url` | string | no | | Any URL of the share to revoke (previewUrl, zipUrl, viewUrl, or revokeUrl). The artifact id is read out of it, so the link you sent someone is enough to pull it back. |
334
+ | `approvalId` | string | no | | The approval handle returned by a prior approval-required response for a revoke. Revoking permanently burns a share and cannot be undone, so when the platform's approval gate is on this needs a human approval: call revoke once without this to get an approval id and URL, have a human approve at extension.dev, then call revoke again with the same id. Listing never needs it. |
335
+ | `projectPath` | string | no | | Path to the extension project root. Reconciles the platform's answer against this project's .extension.dev/shared-previews.json record. Read-only. |
336
+ | `status` | "all" \| "live" | no | `"all"` | all (default) includes expired and revoked shares, which is what makes a dead link explainable; live returns only the links the platform reported resolving at list time (a 429 or 503 from the platform answers listed-local-only, which says nothing about any link). |
337
+ | `limit` | number | no | | How many shares to return, 1 to 200 (platform default 100). A cut list comes back with truncated:true. |
338
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
339
+
340
+ ## extension_start
341
+
342
+ Run the PRODUCTION build in a browser: build the project, serve it, and launch. There is no hot module replacement and no control channel, so your edits are not picked up and extension_eval, extension_storage, extension_reload, extension_open and extension_dom_snapshot cannot attach to this session. Use extension_dev while writing code, and this to check what actually ships. Pass build:false to launch an existing dist/<browser> without rebuilding, or outputPath to launch any prebuilt unpacked extension directory, one another toolchain produced included, which implies build:false.
343
+
344
+ | input | type | required | default | description |
345
+ | --- | --- | --- | --- | --- |
346
+ | `projectPath` | string | yes | | Extension project root |
347
+ | `browser` | "chrome" \| "chromium" \| "edge" \| "brave" \| "opera" \| "vivaldi" \| "yandex" \| "firefox" \| "waterfox" \| "librewolf" \| "zen" \| "floorp" \| "safari" \| "chromium-based" \| "gecko-based" \| "firefox-based" \| "webkit-based" | no | `"chrome"` | |
348
+ | `build` | boolean | no | `true` | Build before serving. false serves the existing dist/<browser> as-is and fails when there is none. |
349
+ | `polyfill` | boolean | no | `true` | Apply cross-browser polyfill (build only) |
350
+ | `port` | number | no | | Passed to the engine as --port (0 for auto-assign). A production start serves nothing over it today; it matters only to a toolchain that reads it. |
351
+ | `noBrowser` | boolean | no | `false` | Build (or, with build:false, check the dist) without launching a browser. A production start serves nothing, so with no browser the engine process ends once the build does; read the result with extension_build rather than a session. |
352
+ | `outputPath` | string | no | | An existing unpacked extension directory to launch as it is (a manifest.json at its root), for an artifact built by another toolchain or an exact release candidate. Implies build:false; projectPath still names the project the session belongs to. Relative paths resolve against projectPath. |
353
+ | `profile` | string | no | | Profile path, or "false" to reuse the real user profile. Omit for a throwaway one. |
354
+ | `startingUrl` | string | no | | URL the browser opens on launch |
355
+ | `chromiumBinary` | string | no | | Custom Chromium-based binary to launch; `browser` still names the target family the engine builds for (chromium-based when none is given). |
356
+ | `geckoBinary` | string | no | | Custom Gecko/Firefox binary to launch; `browser` still names the target family the engine builds for (gecko-based when none is given). |
357
+ | `host` | string | no | | Bind host, default 127.0.0.1. Use 0.0.0.0 in Docker or devcontainers. |
358
+ | `publicHost` | string | no | | Host the browser dials for HMR and reload when it differs from the bind host |
359
+ | `extensions` | array of string | no | | Extra extension paths or store URLs to load alongside the project |
360
+
361
+ ## extension_stop
362
+
363
+ Stop a session that extension_dev or extension_start is running: terminate the server and the browser it launched, and remove the live-preview carrier if extension_dev placed one. This covers extension_start build:false too, which the registry records as a preview session. Call it when you are done verifying, so sessions do not accumulate.
364
+
365
+ | input | type | required | default | description |
366
+ | --- | --- | --- | --- | --- |
367
+ | `projectPath` | string | no | | Extension project root |
368
+ | `browser` | string | no | | Browser of the session to stop. Defaults to the single live session for this project rather than assuming chrome. |
369
+ | `all` | boolean | no | `false` | Stop every known session across projects and browsers, found from this server's registry AND the on-disk markers written by this server or by servers that are no longer running, so it still works after an MCP restart. A marker owned by another MCP server that is still running is left alone and listed under skippedForeign unless includeOtherServers is true. It also takes back every live-preview carrier still recorded on this machine, including one in a project whose session was never stopped. projectPath/browser are then ignored. |
370
+ | `includeOtherServers` | boolean | no | `false` | With all: true, also stop sessions whose markers belong to another MCP server that is still running (the markers sit in a per-user directory every server shares). Off by default, since those sessions are someone else's. |
371
+
372
+ ## extension_storage
373
+
374
+ Read or write chrome.storage in a running extension. Every call runs in the extension's background (the engine honours no context), so it proves nothing about what a content script or page can read. A set is read back and the answer says whether the stored value matches. Start the session with allowControl:true (extension_dev). Set one key per call: there is no bulk-object set.
375
+
376
+ | input | type | required | default | description |
377
+ | --- | --- | --- | --- | --- |
378
+ | `projectPath` | string | yes | | Extension project root (needs a live dev session) |
379
+ | `action` | "get" \| "set" | yes | | get reads a key (or the whole area); set writes a key |
380
+ | `area` | "local" \| "sync" \| "session" \| "managed" | no | `"local"` | |
381
+ | `key` | string | no | | Key to get or set |
382
+ | `value` | any | no | | Value to set (any JSON value); required for action=set |
383
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
384
+ | `timeout` | number | no | | Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port. |
385
+
386
+ ## extension_submit
387
+
388
+ Submit a built extension for store REVIEW through extension.dev, which holds your store credentials and dispatches from your project's mirror CI: the Chrome Web Store, Firefox AMO, Edge Add-ons and the App Store (Safari). This is store review only. It does not push a build to the extension.dev platform, and it does not make a shareable link: that is extension_publish, which is what "deploy" or "ship" an extension almost always means. Reach for this only when the ask is explicitly a store submission. It defaults to a dry run that dispatches nothing: the platform verifies the token and project, finds the store workflow, reads the build from its index when that index is readable, reads store health itself and adds a Safari plan verdict; this tool re-reads the per-store health rows beside it. The dry run does not run the owner gate, the approval, the build quota, the dispatch pause or the submission-mode check, which the real run does first. Pass dryRun:false to actually submit, which is irreversible: only the workspace owner who issued the token may, and it dispatches the store workflow; a Chrome or Edge store with no saved submission mode is uploaded as a draft (absent_mode: safe) and does not enter review. A real submission answers 'submitted' only when the platform recorded a submission for every store asked; 'submitted-partially' names the stores it did not record, which are the only ones to submit again; 'submit-unconfirmed' means no usable answer came back, so read extension_release_status before submitting again, because a second call submits a second time. The project comes from your token (extension_auth or EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page); with several logins stored, `project` picks which one. Store credentials are never arguments, and no local file is uploaded. Call extension_release_status for valid shas, and, after a real submission, for the recorded outcome and review state.
389
+
390
+ | input | type | required | default | description |
391
+ | --- | --- | --- | --- | --- |
392
+ | `project` | string | no | | Which stored login to use, as '<workspace>/<project>' (or a project slug that matches one login), when extension_auth has signed in to more than one project on this machine. Omitted, the most recent login is used, after EXTENSION_DEV_TOKEN when that is set. Named, it outranks EXTENSION_DEV_TOKEN. extension_auth (action: status) lists the stored logins. |
393
+ | `browsers` | array of "chrome" \| "firefox" \| "edge" \| "safari" | yes | | Stores to submit to. |
394
+ | `buildSha` | string | yes | | The built commit SHA to submit. It needs a completed build in the project's build index; an unknown sha is rejected. |
395
+ | `channel` | string | no | | Release channel to submit from (default stable). |
396
+ | `version` | string | no | | Version label for the submission record (optional). |
397
+ | `dryRun` | boolean | no | `true` | Preflight only. Pass false to actually dispatch (irreversible, enters store review). |
398
+ | `projectPath` | string | no | | Path to the extension project root, read only for the local STORE.md advisory check. Nothing local is uploaded; without it the check falls back to the server's working directory. |
399
+ | `approvalId` | string | no | | The approval handle returned by a prior approval-required response for a real submission. A real submission (dryRun:false) is irreversible and needs a human approval when the platform's approval gate is on: call once without this to get an approval id and URL, have a human approve at extension.dev, then call again with the same id. A dry run never needs it. |
400
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
401
+
402
+ ## extension_templates
403
+
404
+ Browse the extension.dev template catalog. Pass action:'list' (the default) to search and filter it and get metadata per template. Pass action:'source' with a `slug` to read one template's files, for learning a pattern before building something similar. Read `framework` as the UI framework only, never the language: TypeScript and JavaScript templates live under slugs ('typescript', 'content-typescript'), shadcn is a React variant ('sidebar-shadcn'), and provider AIs carry the 'ai' tag ('ai-chatgpt', 'ai-claude'). Reach those through query, tags or slug.
405
+
406
+ | input | type | required | default | description |
407
+ | --- | --- | --- | --- | --- |
408
+ | `action` | "list" \| "source" | no | `"list"` | |
409
+ | `slug` | string | no | | source: which template to read (e.g. 'ai-claude', 'content-react'). Required for source. |
410
+ | `files` | array of string | no | | source: paths to read (e.g. ['src/manifest.json']). Omit for the file listing. |
411
+ | `query` | string | no | | list: keyword search over slug, description, tags and useCases. Ranks by word matches, so a natural phrase works. |
412
+ | `surface` | "content" \| "sidebar" \| "newtab" \| "background" | no | | list: filter by surface. For a popup/action starter use query:'action', not a surface. |
413
+ | `framework` | "react" \| "vue" \| "svelte" \| "preact" \| "" | no | | list: UI framework filter (empty string = vanilla JS). |
414
+ | `tags` | array of string | no | | list: filter by tags, e.g. ['ai', 'chat']. |
415
+ | `featured` | boolean | no | | list: only featured templates. |
416
+
417
+ ## extension_theme_verify
418
+
419
+ Verify a Chrome theme manifest before it ships. This settles the four-leg WYSIWYG contract (app-shows == manifest-says == chrome-paints, plus chrome-accepts) as far as is possible headless: it derives every color current Chrome would paint from the manifest through the transcribed Chromium resolver, and classifies each problem as D1 fabrication, D3 parity gap, or D4 acceptance gap (keys Chrome silently discards: dead legacy, incognito, unknown, out-of-range). It verifies only, and never authors or mutates a theme. The app-rendered and real-pixel legs need a browser, so they come back as needsAttended pointing at the assert:theme and install-parity harnesses, never as passed.
420
+
421
+ | input | type | required | default | description |
422
+ | --- | --- | --- | --- | --- |
423
+ | `manifest` | object | no | | The Chrome theme manifest object (with a `theme` block). Pass this or manifestPath. |
424
+ | `manifestPath` | string | no | | Path to a theme manifest.json (or a { manifest } seed wrapper). Read in place of the inline manifest. |
425
+
426
+ ## extension_wait
427
+
428
+ Wait for a running dev or start session to be ready. This polls the ready.json contract and reports compiled (the compiler finished), browserAttached (the runtime executor connected), and guestLoaded (the browser's own target list shows your extension). Read guestLoaded as the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs. It is null when it could not be checked, for example a gecko session with no CDP port. Every result reports budgetMs and elapsedMs; on status 'timeout', call again to keep waiting on the same contract. In a noBrowser session this returns as soon as the compile lands, instead of waiting for a browser that will never attach. Ports come from the contract, so they match what the server actually bound.
429
+
430
+ | input | type | required | default | description |
431
+ | --- | --- | --- | --- | --- |
432
+ | `projectPath` | string | yes | | Extension project root |
433
+ | `browser` | string | no | | Session browser; defaults to this project's live session |
434
+ | `timeoutMs` | number | no | `45000` | Wait budget for this call. Default 45000, clamped to 1000-50000 so one call stays under the client's 60s request timeout. On timeout, call again to keep waiting. |
435
+ | `timeout` | number | no | | Deprecated alias of timeoutMs, which wins when both are given. |
436
+
437
+ ## extension_workspace_create
438
+
439
+ Create an extension.dev workspace that does not exist yet, without opening the console. Use it before extension_project_create when the project's workspace is not there: project creation can only target an existing workspace, and this tool is what brings one into existence. Two-phase, like login: the first call returns a code and a URL where a signed-in GitHub user approves creating exactly this workspace and becomes its owner; call again with the returned deviceCode to finish. Check `ownerGithubLogin` in the answer: whoever approved the code owns the workspace. The approval mints a grant that lives minutes, can only create the one named workspace, names no project, and is never stored on this machine. Then run extension_project_create against '<workspace>/<project>'.
440
+
441
+ | input | type | required | default | description |
442
+ | --- | --- | --- | --- | --- |
443
+ | `workspace` | string | yes | | Slug of the new workspace, lowercase letters, digits and hyphens, no slash. It must not exist yet; a personal workspace (the GitHub login) already exists for every signed-in user, so this is for a shared or organization-style workspace. |
444
+ | `displayName` | string | no | | Human name for the workspace. Defaults to the slug. |
445
+ | `description` | string | no | | Short workspace description. Optional. |
446
+ | `developerUrl` | string | no | | Public URL for the workspace. Optional. |
447
+ | `deviceCode` | string | no | | Resume token from the prior call's `deviceCode`; omit on the first call. |
448
+ | `api` | string | no | | Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev) |
6
449
 
7
- Anthropic has no native browser extension tooling. The extension.dev platform already has clean programmatic APIs (`extensionCreate`, `extensionDev`, `extensionBuild`, `extensionPreview`). Wrapping these as MCP tools makes Claude the first AI that can natively scaffold, develop, build, and test browser extensions.
8
-
9
- For Claude-heavy extension developers, this means: "Build me a Chrome extension that..." just works.
10
-
11
- ## The examples repo as the backbone
12
-
13
- The [examples repo](https://github.com/extension-js/examples) is the template catalog, reference implementation library, and distribution channel for extension.dev. Every MCP tool that creates, recommends, or explains extension patterns should source its knowledge from this repo.
14
-
15
- **Key resource:** `templates-meta.json` (published as a [nightly release asset](https://github.com/extension-js/examples/releases/tag/nightly))
16
-
17
- This file contains structured metadata for every template: surfaces, framework, permissions, entry points, files, download URLs, and SHA256 integrity hashes. It is the single source of truth for what templates exist and what they contain.
18
-
19
- **How `extension create` resolves templates today:**
20
-
21
- ```
22
- User: npx extension create my-ext --template=ai-claude
23
- │
24
- ▼
25
- programs/create/steps/import-external-template.ts
26
- │
27
- ┌────────────────────────────────┤
28
- │ Built-in name? │ Full GitHub URL? │ HTTP zip?
29
- ▼ ▼ ▼
30
- https://github.com/extension-js/ Direct clone axios + adm-zip
31
- examples/tree/main/examples/<slug> via go-git-it
32
- │
33
- ▼
34
- go-git-it clones subtree → copies to project path → cleanup temp
35
- ```
36
-
37
- No caching, every create call re-fetches from GitHub. The MCP server can improve this.
38
-
39
- ---
40
-
41
- ## Tool inventory
42
-
43
- ### Tier 1, Core tools (ship first)
44
-
45
- These map directly to existing programmatic APIs and provide immediate value.
46
-
47
- #### `extension_create`
48
-
49
- **Source:** `programs/create/module.ts` → `extensionCreate()`
50
-
51
- **Purpose:** Scaffold a new browser extension project from a template.
52
-
53
- ```json
54
- {
55
- "name": "extension_create",
56
- "description": "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_templates to see available options.",
57
- "inputSchema": {
58
- "type": "object",
59
- "properties": {
60
- "projectName": {
61
- "type": "string",
62
- "description": "Name of the extension project (used as directory name)"
63
- },
64
- "template": {
65
- "type": "string",
66
- "default": "typescript",
67
- "description": "Template slug from the extension.dev template catalog (e.g. 'react', 'ai-claude', 'content-vue'). Use extension_templates to discover options."
68
- },
69
- "install": {
70
- "type": "boolean",
71
- "default": true,
72
- "description": "Install dependencies after creation"
73
- }
74
- },
75
- "required": ["projectName"]
76
- }
77
- }
78
- ```
79
-
80
- **Returns:** `{ projectPath, projectName, template, depsInstalled }`
81
-
82
- **Integration with examples repo:** The template slug maps directly to a directory under `examples/` in the repo. The tool resolves `https://github.com/extension-js/examples/tree/main/examples/<template>` via `go-git-it`.
83
-
84
- ---
85
-
86
- #### `extension_templates` (action: `"list"`)
87
-
88
- **Source:** New, fetches and queries `templates-meta.json`
89
-
90
- **Purpose:** Search and filter the template catalog. This is how Claude discovers what starting points exist before calling `extension_create`.
91
-
92
- ```json
93
- {
94
- "name": "extension_templates",
95
- "description": "List available extension templates from the extension.dev template catalog. Filter by surface, framework, or tags. Returns structured metadata from templates-meta.json.",
96
- "inputSchema": {
97
- "type": "object",
98
- "properties": {
99
- "surface": {
100
- "type": "string",
101
- "enum": [
102
- "content",
103
- "sidebar",
104
- "action",
105
- "newtab",
106
- "devtools",
107
- "options",
108
- "background"
109
- ],
110
- "description": "Filter by extension surface type"
111
- },
112
- "framework": {
113
- "type": "string",
114
- "enum": ["react", "vue", "svelte", "preact", ""],
115
- "description": "Filter by UI framework (empty string = vanilla JS)"
116
- },
117
- "tags": {
118
- "type": "array",
119
- "items": { "type": "string" },
120
- "description": "Filter by tags (e.g. ['ai', 'chat'])"
121
- },
122
- "featured": {
123
- "type": "boolean",
124
- "description": "Only show featured templates"
125
- },
126
- "query": {
127
- "type": "string",
128
- "description": "Free-text search across slug, description, tags, and useCases"
129
- }
130
- }
131
- }
132
- }
133
- ```
134
-
135
- **Returns:** Array of `{ slug, description, uiFramework, surfaces, tags, difficulty, useCases, repositoryUrl, downloads }`, a filtered view of `templates-meta.json`.
136
-
137
- **Implementation:**
138
-
139
- 1. Fetch `templates-meta.json` from `https://github.com/extension-js/examples/releases/download/nightly/templates-meta.json`
140
- 2. Cache locally (TTL: 1 hour) at `~/.cache/extension-js/templates-meta.json`
141
- 3. Apply filters against the `templates` array
142
- 4. Return matching entries with only the fields Claude needs
143
-
144
- **Why this is Tier 1:** Without this tool, Claude would hard-code template names (which go stale). With it, Claude always knows the current catalog.
145
-
146
- ---
147
-
148
- #### `extension_build`
149
-
150
- **Source:** `programs/develop/module.ts` → `extensionBuild()`
151
-
152
- **Purpose:** Build extension for production/distribution.
153
-
154
- ```json
155
- {
156
- "name": "extension_build",
157
- "description": "Build a browser extension for production. The output lands in dist/<browser>/. Pass zip:true to also package a .zip for store submission. The build refuses a manifest with build-blocking errors unless you pass skipValidation:true, because such a manifest yields a broken bundle the bundler itself never flags.",
158
- "inputSchema": {
159
- "type": "object",
160
- "properties": {
161
- "projectPath": {
162
- "type": "string",
163
- "description": "Path to the extension project root"
164
- },
165
- "browser": {
166
- "type": "string",
167
- "enum": ["chrome", "chromium", "edge", "brave", "opera", "vivaldi", "yandex", "firefox", "waterfox", "librewolf", "safari", "chromium-based", "gecko-based", "firefox-based", "webkit-based"],
168
- "default": "chrome",
169
- "description": "Target browser"
170
- },
171
- "zip": {
172
- "type": "boolean",
173
- "default": false,
174
- "description": "Create a .zip file for store distribution"
175
- },
176
- "zipSource": {
177
- "type": "boolean",
178
- "default": false,
179
- "description": "Include source code zip (required by some stores)"
180
- }
181
- },
182
- "required": ["projectPath"]
183
- }
184
- }
185
- ```
186
-
187
- **Returns:** `{ outputPath, duration, zipPath?, warnings[] }`
188
-
189
- `extension_build` also accepts `zipFilename` (string), `polyfill` (boolean, default false), `silent` (boolean), and `mode` (`development` | `production` | `none`, default `production`).
190
-
191
- ---
192
-
193
- #### `extension_dev`
194
-
195
- **Source:** `programs/develop/module.ts` → `extensionDev()`
196
-
197
- **Purpose:** Start development server with HMR and browser launch.
198
-
199
- ```json
200
- {
201
- "name": "extension_dev",
202
- "description": "Start the extension development server with hot module replacement. Launches a browser with the extension loaded.",
203
- "inputSchema": {
204
- "type": "object",
205
- "properties": {
206
- "projectPath": {
207
- "type": "string",
208
- "description": "Path to the extension project root"
209
- },
210
- "browser": {
211
- "type": "string",
212
- "enum": ["chrome", "chromium", "edge", "brave", "opera", "vivaldi", "yandex", "firefox", "waterfox", "librewolf", "safari", "chromium-based", "gecko-based", "firefox-based", "webkit-based"],
213
- "default": "chrome"
214
- },
215
- "port": {
216
- "type": "number",
217
- "description": "Dev server port (0 for auto-assign)"
218
- },
219
- "noBrowser": {
220
- "type": "boolean",
221
- "default": false,
222
- "description": "Start dev server without launching browser"
223
- },
224
- "polyfill": {
225
- "type": "boolean",
226
- "default": true,
227
- "description": "Apply cross-browser polyfill"
228
- },
229
- "profile": { "type": "string", "description": "Browser profile path, or \"false\" for the default user profile" },
230
- "startingUrl": { "type": "string", "description": "URL the browser opens on launch" },
231
- "chromiumBinary": { "type": "string", "description": "Custom Chromium-based binary path" },
232
- "geckoBinary": { "type": "string", "description": "Custom Gecko/Firefox binary path" },
233
- "host": { "type": "string", "description": "Bind host (0.0.0.0 for Docker); default 127.0.0.1" },
234
- "publicHost": { "type": "string", "description": "Connectable host for HMR/reload when it differs from the bind host" },
235
- "extensions": { "type": "array", "items": { "type": "string" }, "description": "Companion extension paths or store URLs" },
236
- "allowControl": { "type": "boolean", "default": false, "description": "Enable the agent-bridge control channel" },
237
- "allowEval": { "type": "boolean", "default": false, "description": "Additionally enable extension_eval" }
238
- },
239
- "required": ["projectPath"]
240
- }
241
- }
242
- ```
243
-
244
- **Returns:** `{ port, browser, pid }`, long-running process, returns control info.
245
-
246
- ---
247
-
248
- #### `extension_start`
249
-
250
- **Source:** `programs/extension/commands/start.ts` → `extensionBuild()` + `extensionPreview()`
251
-
252
- **Purpose:** Build and launch in production mode (no HMR). The "production test" workflow, builds first, then opens the browser with the built output.
253
-
254
- ```json
255
- {
256
- "name": "extension_start",
257
- "description": "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
258
- "inputSchema": {
259
- "type": "object",
260
- "properties": {
261
- "projectPath": {
262
- "type": "string",
263
- "description": "Path to the extension project root"
264
- },
265
- "browser": {
266
- "type": "string",
267
- "enum": ["chrome", "chromium", "edge", "brave", "opera", "vivaldi", "yandex", "firefox", "waterfox", "librewolf", "safari", "chromium-based", "gecko-based", "firefox-based", "webkit-based"],
268
- "default": "chrome"
269
- },
270
- "polyfill": {
271
- "type": "boolean",
272
- "default": true,
273
- "description": "Apply cross-browser polyfill (default true, unlike dev)"
274
- },
275
- "wait": {
276
- "type": "boolean",
277
- "default": false,
278
- "description": "Wait for ready.json contract and return structured status"
279
- },
280
- "waitTimeout": {
281
- "type": "number",
282
- "default": 60000,
283
- "description": "Timeout in ms when using wait mode"
284
- }
285
- },
286
- "required": ["projectPath"]
287
- }
288
- }
289
- ```
290
-
291
- **Returns:** When `wait: true`, returns the `ready.json` contract: `{ status, browser, port, pid, distPath, manifestPath, compiledAt }`. Otherwise returns `{ pid, browser }`.
292
-
293
- Both `extension_dev` and `extension_start` also accept `port`, `noBrowser`, and the shared launch flags: `profile`, `startingUrl`, `chromiumBinary`, `geckoBinary`, `host`, `publicHost`, `extensions` (same shapes as on `extension_dev`).
294
-
295
- **Why this is distinct from dev:** `dev` uses HMR and watches files. `start` builds once in production mode and launches, what you'd use to verify a production build works before publishing.
296
-
297
- ---
298
-
299
- #### `extension_start` (build: `false`)
300
-
301
- **Source:** `programs/develop/module.ts` → `extensionPreview()`
302
-
303
- **Purpose:** Preview a built extension without dev server.
304
-
305
- ```json
306
- {
307
- "name": "extension_start",
308
- "description": "Preview a production-built extension in a browser. Uses dist/ output directly.",
309
- "inputSchema": {
310
- "type": "object",
311
- "properties": {
312
- "projectPath": {
313
- "type": "string",
314
- "description": "Path to the extension project root"
315
- },
316
- "browser": {
317
- "type": "string",
318
- "enum": ["chrome", "chromium", "edge", "brave", "opera", "vivaldi", "yandex", "firefox", "waterfox", "librewolf", "safari", "chromium-based", "gecko-based", "firefox-based", "webkit-based"],
319
- "default": "chrome"
320
- }
321
- },
322
- "required": ["projectPath"]
323
- }
324
- }
325
- ```
326
-
327
- ---
328
-
329
- ### Tier 2, Intelligence tools (high DX value)
330
-
331
- These combine extension.dev knowledge with the examples repo to make Claude _smart_ about extensions, not just a CLI wrapper.
332
-
333
- #### `extension_templates` (action: `"source"`)
334
-
335
- **Source:** New, reads files from the examples repo
336
-
337
- **Purpose:** Read the source code of a template to learn its patterns before building something similar. This is how Claude learns extension patterns by example rather than from documentation.
338
-
339
- ```json
340
- {
341
- "name": "extension_templates",
342
- "description": "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
343
- "inputSchema": {
344
- "type": "object",
345
- "properties": {
346
- "slug": {
347
- "type": "string",
348
- "description": "Template slug (e.g. 'ai-claude', 'content-react')"
349
- },
350
- "files": {
351
- "type": "array",
352
- "items": { "type": "string" },
353
- "description": "Specific files to read (e.g. ['src/manifest.json', 'src/background.ts']). If omitted, returns the file listing from templates-meta.json."
354
- }
355
- },
356
- "required": ["slug"]
357
- }
358
- }
359
- ```
360
-
361
- **Implementation:**
362
-
363
- 1. Look up the template in `templates-meta.json` to get its `files` array and `repositoryUrl`
364
- 2. If `files` param is omitted: return the file listing + metadata (surfaces, framework, permissions)
365
- 3. If `files` param is provided: fetch each file from `https://raw.githubusercontent.com/extension-js/examples/main/examples/<slug>/<file>`
366
- 4. Return file contents alongside the template metadata for context
367
-
368
- **Why this matters:** When a user says "add a sidebar like the shadcn example," Claude can read the actual `sidebar-shadcn` source (its manifest structure, background script pattern, component layout) and replicate it accurately. The examples repo becomes a living pattern library for Claude.
369
-
370
- **Advanced pattern learning:** Complex content script patterns (multi-level imports, MAIN world) can be learned from `content-multi-one-entry`, `content-multi-three-entries`, and `content-main-world` example sources.
371
-
372
- ---
373
-
374
- #### `extension_manifest_validate`
375
-
376
- **Source:** New tool, wraps manifest parsing logic from `plugin-web-extension`
377
-
378
- **Purpose:** Validate and explain issues in a manifest.json.
379
-
380
- ```json
381
- {
382
- "name": "extension_manifest_validate",
383
- "description": "Validate a manifest.json file for correctness across browsers. Reports missing fields, invalid permissions, and cross-browser compatibility issues. Cross-references against known-good manifests in the template catalog.",
384
- "inputSchema": {
385
- "type": "object",
386
- "properties": {
387
- "manifestPath": {
388
- "type": "string",
389
- "description": "Path to manifest.json"
390
- },
391
- "browsers": {
392
- "type": "array",
393
- "items": { "type": "string" },
394
- "default": ["chrome", "firefox"],
395
- "description": "Browsers to validate against"
396
- }
397
- },
398
- "required": ["manifestPath"]
399
- }
400
- }
401
- ```
402
-
403
- **Returns:** `{ valid, errors[], warnings[], browserSupport: { chrome: {}, firefox: {} }, similarTemplates[] }`
404
-
405
- The `similarTemplates` field lists templates from the catalog with similar surfaces/permissions, useful for cross-referencing a known-good example.
406
-
407
- ---
408
-
409
- #### `extension_analyze`
410
-
411
- **Source:** New tool; static analysis of the built `dist/` output
412
-
413
- **Purpose:** Analyze a built extension's structure, size, and entry points.
414
-
415
- ```json
416
- {
417
- "name": "extension_analyze",
418
- "description": "Analyze a built extension: file sizes, entry points, permissions used, and dependency analysis.",
419
- "inputSchema": {
420
- "type": "object",
421
- "properties": {
422
- "projectPath": {
423
- "type": "string",
424
- "description": "Path to the extension project root"
425
- },
426
- "browser": {
427
- "type": "string",
428
- "default": "chrome"
429
- },
430
- "format": {
431
- "type": "string",
432
- "enum": ["summary", "tree", "json"],
433
- "default": "summary"
434
- }
435
- },
436
- "required": ["projectPath"]
437
- }
438
- }
439
- ```
440
-
441
- **Returns:** File tree with sizes, entry point map, permissions analysis, estimated store review flags.
442
-
443
- ---
444
-
445
- #### `extension_add_feature`
446
-
447
- **Source:** New tool, codegen based on examples repo patterns
448
-
449
- **Purpose:** Add a feature surface to an existing extension (sidebar, content script, popup, options page, etc.)
450
-
451
- ```json
452
- {
453
- "name": "extension_add_feature",
454
- "description": "Add a new feature surface to an existing extension. Generates the required files and updates manifest.json. Uses patterns from the extension.dev template catalog.",
455
- "inputSchema": {
456
- "type": "object",
457
- "properties": {
458
- "projectPath": {
459
- "type": "string"
460
- },
461
- "feature": {
462
- "type": "string",
463
- "enum": [
464
- "sidebar",
465
- "popup",
466
- "options",
467
- "content-script",
468
- "background",
469
- "newtab",
470
- "devtools",
471
- "history",
472
- "bookmarks"
473
- ],
474
- "description": "Feature surface to add"
475
- },
476
- "framework": {
477
- "type": "string",
478
- "enum": ["react", "vue", "svelte", "preact", "vanilla"],
479
- "default": "react"
480
- }
481
- },
482
- "required": ["projectPath", "feature"]
483
- }
484
- }
485
- ```
486
-
487
- **Implementation:** Internally calls `extension_templates` with `action: "source"` to fetch the canonical pattern for the requested surface+framework combination, then generates the files and updates manifest.json. The examples repo is the codegen source, not hard-coded templates.
488
-
489
- ---
490
-
491
- #### `extension_inspect`
492
-
493
- **Source:** `programs/extension/browsers/` → CDP/RDP source inspection system
494
-
495
- **Purpose:** Live-inspect a running extension's DOM, console, and content script injection state over the debugging protocol. It gives Claude _eyes_ into the running extension. (This grew out of the engine's old `dev --source` CLI flags, which are no longer registered; the MCP connects to the CDP port directly.)
496
-
497
- ```json
498
- {
499
- "name": "extension_inspect",
500
- "description": "Inspect a running extension's live state: DOM structure, content script injection, console messages, and selector queries. Requires an active dev or start session.",
501
- "inputSchema": {
502
- "type": "object",
503
- "properties": {
504
- "projectPath": {
505
- "type": "string",
506
- "description": "Path to the extension project root (must have an active dev session)"
507
- },
508
- "url": {
509
- "type": "string",
510
- "description": "URL to inspect (navigates the browser tab). Defaults to the current tab."
511
- },
512
- "probe": {
513
- "type": "array",
514
- "items": { "type": "string" },
515
- "description": "CSS selectors to query. Returns element counts and samples for each"
516
- },
517
- "include": {
518
- "type": "array",
519
- "items": {
520
- "type": "string",
521
- "enum": ["html", "summary", "meta", "dom_snapshot", "console", "extension_roots"]
522
- },
523
- "default": ["summary", "meta", "console"],
524
- "description": "What data to return"
525
- },
526
- "context": {
527
- "type": "string",
528
- "enum": [
529
- "page",
530
- "options",
531
- "sidepanel",
532
- "devtools",
533
- "newtab",
534
- "popup",
535
- "background"
536
- ],
537
- "default": "page",
538
- "description": "Extension context to inspect (page = content script on web page)"
539
- },
540
- "shadowDom": {
541
- "type": "string",
542
- "enum": ["off", "open-only", "all"],
543
- "default": "open-only",
544
- "description": "Shadow DOM traversal strategy"
545
- },
546
- "redact": {
547
- "type": "string",
548
- "enum": ["off", "safe", "strict"],
549
- "default": "safe",
550
- "description": "Redact sensitive content from HTML output"
551
- }
552
- },
553
- "required": ["projectPath"]
554
- }
555
- }
556
- ```
557
-
558
- **Returns:** One JSON object; keys appear based on `include` selection:
559
-
560
- | Key | What it contains |
561
- | ---------------- | ------------------------------------------------------------------------------------------ |
562
- | `html` | Full injected HTML (after content scripts run), `htmlTruncated` flag when capped |
563
- | `summary` | Compact stats: html length, script/style/link counts, extension root + body child counts |
564
- | `meta` | readyState, viewport dimensions, frame count |
565
- | `domSnapshot` | Structured tree: tag, id, classes, role, text length, child count (max 500 nodes, depth 6) |
566
- | `console` | error/warn/info/log/debug counts + top 5 unique messages |
567
- | `extensionRoots` | Extension root elements with reinject generations |
568
- | `probes` | Per-selector: count + element samples (when `probe` selectors are passed) |
569
-
570
- **Implementation:**
571
-
572
- - Chromium: Uses the existing CDP client (`CDPClient.evaluate()`, `CDPClient.getPageHTML()`) via the already-running dev session's remote debugging port
573
- - Firefox: Uses the existing RDP transport via the already-running dev session
574
- - The `context` parameter maps to the Phase A expansion in `SESSION-SOURCE-EXTENSION-CONTEXTS.md`, currently only `page` works; extension UI contexts (`options`, `sidepanel`, `popup`, etc.) are planned
575
-
576
- **Why this is the highest-value Tier 2 tool:** Claude can't fix what it can't see. When a content script doesn't inject, when a selector doesn't match, when the console is full of errors, this tool tells Claude exactly what's happening in the live browser. For complex multi-level content script chains, `probe: ["[data-extension-root]"]` instantly shows whether injection succeeded.
577
-
578
- ---
579
-
580
- #### `extension_wait`
581
-
582
- **Source:** `programs/extension/commands/dev-wait.ts`
583
-
584
- **Purpose:** Poll for extension readiness after `dev` or `start`. Returns structured status from the `ready.json` contract.
585
-
586
- ```json
587
- {
588
- "name": "extension_wait",
589
- "description": "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
590
- "inputSchema": {
591
- "type": "object",
592
- "properties": {
593
- "projectPath": {
594
- "type": "string",
595
- "description": "Path to the extension project root"
596
- },
597
- "browser": {
598
- "type": "string",
599
- "default": "chrome",
600
- "description": "Browser to check readiness for"
601
- },
602
- "timeout": {
603
- "type": "number",
604
- "default": 60000,
605
- "description": "Timeout in milliseconds"
606
- }
607
- },
608
- "required": ["projectPath"]
609
- }
610
- }
611
- ```
612
-
613
- **Returns:** The schema-1 envelope, carrying the `ready.json` contract under `value`:
614
-
615
- ```json
616
- {
617
- "schema": 1,
618
- "ok": true,
619
- "command": "extension_wait",
620
- "status": "ready",
621
- "value": {
622
- "compiled": true,
623
- "browserAttached": true,
624
- "sessionCommand": "dev",
625
- "browser": "chrome",
626
- "port": 8080,
627
- "pid": 12345,
628
- "distPath": "/path/to/dist/chrome",
629
- "manifestPath": "/path/to/dist/chrome/manifest.json",
630
- "compiledAt": "2026-04-14T10:30:00.000Z",
631
- "startedAt": "2026-04-14T10:29:55.000Z"
632
- },
633
- "error": null,
634
- "warnings": []
635
- }
636
- ```
637
-
638
- The envelope's `command` names the tool, so the ready contract's own `command`
639
- is carried as `value.sessionCommand`.
640
-
641
- **Why this matters for MCP:** When Claude starts a dev session via `extension_dev`, it needs to know when the extension is actually loaded and ready before calling `extension_inspect`. This tool provides that gate.
642
-
643
- ---
644
-
645
- #### `extension_stop`
646
-
647
- **Source:** MCP `lib/process-manager` + the `ready.json` contract (no CLI verb)
648
-
649
- **Purpose:** Terminate a running dev/start/preview session, the dev server AND the browser it launched. The lifecycle counterpart to `extension_dev`/`extension_start`.
650
-
651
- ```json
652
- {
653
- "name": "extension_stop",
654
- "description": "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched.",
655
- "inputSchema": {
656
- "type": "object",
657
- "properties": {
658
- "projectPath": {
659
- "type": "string",
660
- "description": "Path to the extension project root"
661
- },
662
- "browser": {
663
- "type": "string",
664
- "default": "chrome",
665
- "description": "Browser of the session to stop"
666
- },
667
- "all": {
668
- "type": "boolean",
669
- "default": false,
670
- "description": "Stop every session this server started"
671
- }
672
- },
673
- "required": []
674
- }
675
- }
676
- ```
677
-
678
- **Returns:** `{ projectPath, browser, pid, stopped, detail }` (or `{ stopped: [...] }` with `all: true`).
679
-
680
- **How it finds the process:** the in-memory session registry first; if the MCP server restarted since the session began, it falls back to the `pid` recorded in the `ready.json` contract. It signals the whole process group (sessions are spawned detached), escalates SIGTERM → SIGKILL, and removes the stale `ready.json` so a later `extension_wait` cannot report a dead session as ready.
681
-
682
- **Why this matters for MCP:** without a stop tool, every `extension_dev` call leaks a dev server and a browser window that outlive the agent's task. Agents should stop sessions when verification is done.
683
-
684
- ---
685
-
686
- ### Tier 3, Browser management tools
687
-
688
- #### `extension_browsers` (action: `"install"`)
689
-
690
- **Source:** `programs/install/module.ts` → `extensionInstall()`
691
-
692
- **Purpose:** Install managed browser binaries for testing.
693
-
694
- ```json
695
- {
696
- "name": "extension_browsers",
697
- "description": "Install a managed browser binary for extension testing. Useful in CI or fresh environments.",
698
- "inputSchema": {
699
- "type": "object",
700
- "properties": {
701
- "browser": {
702
- "type": "string",
703
- "enum": ["chrome", "chromium", "edge", "firefox"]
704
- }
705
- },
706
- "required": ["browser"]
707
- }
708
- }
709
- ```
710
-
711
- #### `extension_browsers` (action: `"list"`)
712
-
713
- **Source:** `programs/install/module.ts` → `getManagedBrowsersCacheRoot()`
714
-
715
- **Purpose:** List installed managed browsers and their paths.
716
-
717
- ---
718
-
719
- #### `extension_browsers` (action: `"detect"`)
720
-
721
- **Source:** `programs/extension/browsers/` → binary resolution chain
722
-
723
- **Purpose:** Detect which browsers are available on the system and their binary paths. Uses the same resolution chain as the CLI: managed cache → WSL → custom binary → npm location packages (`chrome-location2`, `firefox-location2`, `edge-location`).
724
-
725
- ```json
726
- {
727
- "name": "extension_browsers",
728
- "description": "Detect which browsers are available for extension development. Returns paths and capabilities for each detected browser.",
729
- "inputSchema": {
730
- "type": "object",
731
- "properties": {
732
- "browsers": {
733
- "type": "array",
734
- "items": {
735
- "type": "string",
736
- "enum": ["chrome", "chromium", "edge", "firefox"]
737
- },
738
- "description": "Browsers to check. If omitted, checks all."
739
- }
740
- }
741
- }
742
- }
743
- ```
744
-
745
- **Returns:**
746
-
747
- ```json
748
- {
749
- "detected": [
750
- {
751
- "browser": "chrome",
752
- "binaryPath": "/usr/bin/google-chrome",
753
- "source": "system",
754
- "engine": "chromium",
755
- "cdpSupport": true
756
- },
757
- {
758
- "browser": "firefox",
759
- "binaryPath": "/usr/bin/firefox",
760
- "source": "system",
761
- "engine": "gecko",
762
- "cdpSupport": false,
763
- "rdpSupport": true
764
- },
765
- {
766
- "browser": "safari",
767
- "binaryPath": "/Applications/Safari.app/Contents/MacOS/Safari",
768
- "source": "system",
769
- "engine": "webkit",
770
- "version": "27.0",
771
- "cdpSupport": false,
772
- "rdpSupport": false,
773
- "automation": {
774
- "safaridriver": "/usr/bin/safaridriver",
775
- "mcp": true,
776
- "bidi": true
777
- }
778
- }
779
- ],
780
- "managed": {
781
- "cacheRoot": "/home/user/.cache/extension.js/browsers",
782
- "installed": ["chromium"]
783
- }
784
- }
785
- ```
786
-
787
- `automation` appears on Safari only (macOS): whether the safaridriver beside it speaks `--mcp` (Apple's Safari MCP server, Safari 27+) and `--bidi`. The hint says how to pair that server when it is there.
788
-
789
- **Why this matters:** Before Claude runs `extension_dev --browser=firefox`, it should know if Firefox is actually installed. This prevents "browser not found" errors and lets Claude suggest `extension_browsers` when needed. Especially important for Docker/devcontainer environments.
790
-
791
- ---
792
-
793
- #### `extension_browsers` (action: `"uninstall"`)
794
-
795
- **Source:** `extension-install` → `extensionUninstall()`
796
-
797
- **Purpose:** Remove a managed browser binary from the Extension.js cache (never system-installed browsers).
798
-
799
- ```json
800
- {
801
- "name": "extension_browsers",
802
- "inputSchema": {
803
- "type": "object",
804
- "properties": {
805
- "browser": {
806
- "type": "string",
807
- "enum": ["chrome", "chromium", "edge", "firefox"],
808
- "description": "Managed browser to remove"
809
- },
810
- "all": {
811
- "type": "boolean",
812
- "default": false,
813
- "description": "Remove every managed browser binary"
814
- }
815
- },
816
- "required": []
817
- }
818
- }
819
- ```
820
-
821
- **Returns:** `{ status, target, duration }`
822
-
823
- ---
824
-
825
- ## Where each tool lives in the codebase
826
-
827
- | MCP Tool | Program | Source API | Data source | Needs new code? |
828
- | ------------------------------- | -------------------- | ----------------------------------------- | ------------------------------------------- | ------------------------------------------ |
829
- | **Tier 1, Core** | | | | |
830
- | `extension_create` | `programs/create` | `extensionCreate()` | examples repo via go-git-it | Thin wrapper only |
831
- | `extension_templates` | New | n/a | `templates-meta.json` release asset + raw GitHub | `list` fetches, filters and caches; `source` reads files |
832
- | `extension_build` | `programs/develop` | `extensionBuild()` | n/a | Thin wrapper only |
833
- | `extension_dev` | `programs/develop` | `extensionDev()` | n/a | Thin wrapper + process management |
834
- | `extension_start` | `programs/extension` | `extensionBuild()` + `extensionPreview()` | n/a | Thin wrapper; `build: false` calls `extensionPreview()` alone |
835
- | **Tier 2, Intelligence** | | | | |
836
- | `extension_manifest_validate` | `programs/develop` | `plugin-web-extension` | `templates-meta.json` for similar templates | Extract validation logic |
837
- | `extension_analyze` | `programs/develop` | `--source` flag logic | n/a | Extract into callable API |
838
- | `extension_inspect` | `programs/extension` | CDP client / RDP transport | Live browser via debugging protocol | Wire to running session |
839
- | `extension_list_extensions` | MCP `lib/cdp` | `Extensions.getExtensionInfo` (read-only) | Live browser via CDP (Chromium) | MCP tool (no CLI verb) |
840
- | `extension_wait` | `programs/extension` | `dev-wait.ts` | `ready.json` contract file | Thin wrapper (exists in CLI) |
841
- | `extension_stop` | MCP `lib/process-manager` | session registry + group signal | Session registry + `ready.json` pid | MCP tool (no CLI verb) |
842
- | `extension_add_feature` | New | `extension_templates` | examples repo patterns | Codegen from examples |
843
- | **Agent bridge, act / triggers** | | | | |
844
- | `extension_eval` | `programs/extension` | bridge control channel | Live extension context | Wraps `extension eval` (`--allow-eval`) |
845
- | `extension_storage` | `programs/extension` | bridge control channel | `chrome.storage` | Wraps `extension storage` |
846
- | `extension_reload` | `programs/extension` | bridge control channel | Live extension | Wraps `extension reload` |
847
- | `extension_open` | `programs/extension` | bridge control channel | Surfaces + `action`/`command` replay | Wraps `extension open` |
848
- | `extension_logs` | `programs/extension` | bridge log/control channel | `logs.ndjson` + live channel | Wraps `extension logs` |
849
- | **Tier 3, Browser management** | | | | |
850
- | `extension_browsers` | `programs/install` | `extensionInstall()`, `getManagedBrowsersCacheRoot()`, binary resolution chain | System PATH + managed cache | One tool, four actions |
851
-
852
- ## Changes needed in existing programs
853
-
854
- ### `programs/develop/`
855
-
856
- - **Extract manifest validation** from `plugin-web-extension` into a standalone callable function. Currently validation is embedded in the Rspack plugin lifecycle.
857
- - **Extract source inspection** from `--source` flag handling into `extensionInspect(projectPath, options)` API.
858
- - **Add `--json` output mode** to `extensionBuild()` return value. Currently returns `BuildSummary` but it could be richer with file-level details.
859
- - **Structured error types.** Current errors are human-readable strings. MCP tools need error codes + structured details for Claude to act on.
860
-
861
- ### `programs/create/`
862
-
863
- - **Template listing API.** Add `extensionListTemplates(filters?)` that fetches+caches `templates-meta.json` and returns filtered results. This serves both the MCP `extension_templates` tool and any future CLI `extension list` command.
864
- - **Dry-run mode.** Add `dryRun` option to `extensionCreate()` that returns the file list without writing. Useful for Claude to explain what will be created before doing it.
865
- - **Template caching.** The current no-cache approach (re-download from GitHub every time) works but is slow. An MCP server that handles many create calls should cache the examples repo or individual template tarballs with a TTL.
866
-
867
- ### `programs/install/`
868
-
869
- - **Already clean.** `extensionInstall()` and `extensionUninstall()` are ready for wrapping.
870
-
871
- ### `programs/extension/` (CLI + browsers)
872
-
873
- - **`--json` flag for all commands.** Machine-readable output for every command. This benefits not just MCP but any programmatic consumer. The `--ai-help` / `--format json` flags already exist, extend this pattern to command output.
874
- - **Exit codes.** Ensure distinct exit codes for different failure modes (missing manifest, build error, browser not found, etc.)
875
- - **`extension list` command.** Expose `extensionListTemplates()` as a CLI command. Shows the catalog in terminal or JSON.
876
- - **Extract binary detection into callable API.** The browser resolution chain (managed cache → WSL → custom binary → npm location packages) is embedded in `chromium-launch/index.ts` and `firefox-launch/index.ts`. Extract into `extensionDetectBrowsers()` for the `extension_browsers` MCP tool.
877
- - **Extract source inspection into MCP-callable API.** The `--source` system is deeply integrated into the browser launch lifecycle. For MCP, we need a way to call it against an _already-running_ dev session. The ready.json contract already gives us port/pid, the MCP server can connect to the CDP/RDP port directly.
878
- - **Extract wait mode into callable API.** The `dev-wait.ts` logic is CLI-only. Expose `extensionWait(projectPath, browser, timeout)` as a programmatic function.
879
- - **Expose the `start` command programmatically.** Currently `start` is CLI-only orchestration (build then preview). Add `extensionStart()` that chains `extensionBuild()` + `extensionPreview()` with the ready.json contract.
880
-
881
- ### Examples repo
882
-
883
- - **AI metadata in `template.meta.json`.** See "AI-relevant metadata fields" section below.
884
- - **Ensure `templates-meta.json` is always published** as a release asset and committed to the repo, so both the MCP server and Claude Code rules can consume it.
885
-
886
- ---
887
-
888
- ## AI-relevant metadata fields
889
-
890
- Proposed additions to the `template.meta.json` curated schema (via `CURATED_ALLOWED_KEYS` in `generate-templates-meta.mjs`):
891
-
892
- ```javascript
893
- const CURATED_ALLOWED_KEYS = [
894
- // Existing
895
- "title",
896
- "featured",
897
- "tags",
898
- "difficulty",
899
- "timeToFirstSuccessMinutes",
900
- "firstSteps",
901
- "useCases",
902
- "docsUrl",
903
- // Proposed additions
904
- "aiPromptExamples", // Example user prompts this template is good for
905
- "aiRecommendFor", // Keywords/intents that should recommend this template
906
- "patternExplanation", // Brief explanation of the architectural pattern
907
- "keyFiles", // Most important files to read to understand the pattern
908
- ];
909
- ```
910
-
911
- **Example for `ai-claude/template.meta.json`:**
912
-
913
- ```json
914
- {
915
- "title": "Claude AI Sidebar",
916
- "featured": true,
917
- "tags": ["ai", "claude", "anthropic", "chat", "sidebar", "react", "shadcn"],
918
- "difficulty": "beginner",
919
- "timeToFirstSuccessMinutes": 3,
920
- "useCases": [
921
- "AI assistant sidebar for any webpage",
922
- "Claude-powered research companion"
923
- ],
924
- "aiPromptExamples": [
925
- "Build a Chrome extension with a Claude chatbot sidebar",
926
- "Create a browser extension that lets me talk to AI on any page",
927
- "Make an extension with an Anthropic-powered assistant panel"
928
- ],
929
- "aiRecommendFor": [
930
- "claude",
931
- "anthropic",
932
- "ai chat",
933
- "llm sidebar",
934
- "ai assistant"
935
- ],
936
- "patternExplanation": "Sidebar panel with React chat UI calling Anthropic SDK. API key stored in chrome.storage.local. Cross-browser via chromium:side_panel + firefox:sidebar_action.",
937
- "keyFiles": [
938
- "src/manifest.json",
939
- "src/lib/claude.ts",
940
- "src/sidebar/SidebarApp.tsx",
941
- "src/background.ts"
942
- ]
943
- }
944
- ```
945
-
946
- These fields enable `extension_templates` to match user intent ("I want to build an AI sidebar") to the right template with `action: "list"`, and to read only the key files with `action: "source"`.
947
-
948
- ---
949
-
950
- ## Implementation plan
951
-
952
- ### Phase 1: Foundation (changes to existing programs)
953
-
954
- 1. Add `--json` output flag to build/dev/start/preview/create commands in `programs/extension`
955
- 2. Extract manifest validation into `extensionValidateManifest()` in `programs/develop`
956
- 3. Add `extensionListTemplates(filters?)` to `programs/create`, fetches and caches `templates-meta.json`
957
- 4. Add `extension list` CLI command wrapping the above
958
- 5. Extract browser detection into `extensionDetectBrowsers()` in `programs/extension`
959
- 6. Extract wait mode into `extensionWait()` in `programs/extension`
960
- 7. Add AI metadata fields to `CURATED_ALLOWED_KEYS` in `generate-templates-meta.mjs`
961
- 8. Populate `template.meta.json` with AI fields for key templates (ai-claude, ai-chatgpt, sidebar-transformers-js)
962
-
963
- ### Phase 2: MCP Server package
964
-
965
- 1. New package: `programs/mcp` or standalone `@extension.dev/mcp`
966
- 2. Implement Tier 1 tools: `extension_create`, `extension_templates`, `extension_build`, `extension_dev`, `extension_start`
967
- 3. `extension_templates` caches `templates-meta.json` with 1-hour TTL
968
- 4. Register on MCP directory (npmjs.com + modelcontextprotocol.io)
969
-
970
- ### Phase 3: Live inspection tools
971
-
972
- 1. `extension_wait`, poll ready.json contract (gate for inspection tools)
973
- 2. `extension_inspect`, connect to running session's CDP/RDP port for live DOM inspection
974
- 3. `extension_browsers`, system browser detection
975
- 4. `extension_templates` `action: "source"`, reads from examples repo via raw.githubusercontent.com
976
- 5. `extension_manifest_validate`, cross-browser validation + similar template suggestions
977
-
978
- ### Phase 4: Codegen + advanced tools
979
-
980
- 1. `extension_analyze`, static build analysis from `--source` extraction
981
- 2. `extension_add_feature`, codegen sourced from examples repo patterns
982
-
983
- ### Phase 5: Feedback loop
984
-
985
- 1. MCP server reports which templates Claude recommends most → feed into `featured` rankings
986
- 2. Track which `aiPromptExamples` lead to successful creates → improve matching
987
- 3. New templates added to examples repo are immediately available via `extension_templates` (no MCP server update needed, it reads `templates-meta.json` at runtime)
988
-
989
- ---
990
-
991
- ## DX priorities for power users
992
-
993
- Typical power-user workflows that drive tool prioritization:
994
-
995
- - Multi-browser extensions (Chrome + Firefox)
996
- - Complex content script import trees (multi-level chains across manifest entries)
997
- - Docker/devcontainer development
998
- - Heavy Claude Code usage for rapid iteration
999
-
1000
- **Highest-value tools by workflow:**
1001
-
1002
- | Workflow | Tool | Why |
1003
- | ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
1004
- | Debugging injection failures | `extension_inspect` | `probe: ["[data-extension-root]"]` shows injection state, reinject generation, console errors, no manual DevTools needed |
1005
- | Docker/devcontainer | `extension_browsers` + `extension_wait` | Check browser availability, gate on dev server readiness |
1006
- | Multi-browser | `extension_manifest_validate` + `extension_build` | Catch manifest divergence early, build for `chrome,firefox` |
1007
- | Learning patterns | `extension_templates` (`list` then `source`) | Read `content-multi-one-entry`, `content-multi-three-entries` for multi-level import patterns |
1008
- | Rapid prototyping | `extension_add_feature` | "Add a sidebar" generates correct manifest + files + background handler |
1009
-
1010
- **Why the examples repo is central:** Complex patterns (multi-level content script imports, MAIN world isolation, cross-browser sidebars) are documented as working examples. `extension_templates` gives Claude the canonical implementation to reference when building or debugging these patterns.
1011
-
1012
- **Why source inspection is the highest-value tool:** The most time-consuming extension debugging failure is "it didn't load." `extension_inspect` with `probe` and `console_summary` turns manual Chrome DevTools investigation into a one-call Claude diagnosis.