@extension.dev/mcp 4.1.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +22 -2
- package/claude/rules/mcp-tools.md +42 -0
- package/dist/module.js +263 -22
- package/dist/src/index.d.ts +9 -0
- package/dist/src/lib/exec.d.ts +16 -0
- package/dist/src/lib/types.d.ts +1 -1
- package/dist/src/lib/validate-input.d.ts +11 -0
- package/dist/src/tools/create.d.ts +5 -0
- package/dist/src/tools/stop.d.ts +29 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
Session lifecycle + determinism release. Tool count 27 -> 28.
|
|
6
|
+
|
|
7
|
+
- New tool `extension_stop`: terminates a dev/start/preview session (dev
|
|
8
|
+
server AND the browser it launched) via a process-group signal with
|
|
9
|
+
SIGTERM -> SIGKILL escalation. Finds the pid in the in-memory session
|
|
10
|
+
registry, falling back to the `ready.json` contract when the MCP server
|
|
11
|
+
restarted since the session began, and removes the stale contract so
|
|
12
|
+
`extension_wait` cannot report a dead session as ready. Supports
|
|
13
|
+
`all: true` to stop everything the server started.
|
|
14
|
+
- Sessions self-clean: dev/start/preview register an exit listener so a
|
|
15
|
+
session that dies on its own is no longer reported as stoppable.
|
|
16
|
+
`extension_preview` sessions are now registered (and stoppable) too.
|
|
17
|
+
- `extension_create` gains `parentDir`: control where the project lands
|
|
18
|
+
instead of inheriting the MCP server's working directory. `nextSteps`
|
|
19
|
+
now reports the full project path.
|
|
20
|
+
- CLI spawns are deterministic: dev/start/preview and the act tools now
|
|
21
|
+
prefer the project's own `node_modules/.bin/extension`, falling back to
|
|
22
|
+
`npx extension@<pinned>` where the pin derives from the vendored
|
|
23
|
+
`extension-develop` version — never a floating `latest`.
|
|
24
|
+
- Session registry keys are path-normalized, so a stop with an absolute
|
|
25
|
+
path matches a session registered with a relative one.
|
|
26
|
+
- Tests: registry suite now asserts against the exported `tools` array
|
|
27
|
+
(the old hand-maintained mirror had drifted to 26 while the server
|
|
28
|
+
registered 27); new stop + CLI-resolution suites (123 tests total).
|
|
29
|
+
|
|
30
|
+
## 4.1.2
|
|
31
|
+
|
|
32
|
+
README: restore the `@extension.dev/skill` pairing section (hands +
|
|
33
|
+
judgment) now that the skill is public on npm. No code changes.
|
|
34
|
+
|
|
3
35
|
## 4.1.1
|
|
4
36
|
|
|
5
37
|
README rewritten for the public npm page: Extension.js-style header
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# @extension.dev/mcp [![Version][npm-version-image]][npm-version-url] [![Downloads][npm-downloads-image]][npm-downloads-url] [![Discord][discord-image]][discord-url]
|
|
9
9
|
|
|
10
|
-
> Give your AI agent hands for browser extension development.
|
|
10
|
+
> Give your AI agent hands for browser extension development. 28 MCP tools that scaffold, run, inspect, debug, and publish cross-browser extensions.
|
|
11
11
|
|
|
12
12
|
<img alt="Logo" align="right" src="https://avatars.githubusercontent.com/u/106714027" width="20%" />
|
|
13
13
|
|
|
@@ -54,6 +54,15 @@ claude mcp add extension-dev npx @extension.dev/mcp
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### Pair with the skill
|
|
58
|
+
|
|
59
|
+
This server gives agents hands; [`@extension.dev/skill`](https://www.npmjs.com/package/@extension.dev/skill) gives them judgment: the cross-browser rules, silent-failure gotchas, debugging playbooks, and store checklist, packaged in the open [Agent Skills](https://agentskills.io) format. With both installed, agents know to verify against the live browser instead of guessing, and these tools make that a one-call operation.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm i -D @extension.dev/skill
|
|
63
|
+
mkdir -p .claude/skills && cp -R node_modules/@extension.dev/skill/skills/extension-dev .claude/skills/
|
|
64
|
+
```
|
|
65
|
+
|
|
57
66
|
### Claude Code project integration
|
|
58
67
|
|
|
59
68
|
The package ships drop-in instructions, slash commands, and rules for extension projects:
|
|
@@ -80,6 +89,7 @@ cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/c
|
|
|
80
89
|
| run | `extension_start` | Build + preview |
|
|
81
90
|
| run | `extension_preview` | Preview the production build |
|
|
82
91
|
| run | `extension_wait` | Poll the dev-server ready contract |
|
|
92
|
+
| run | `extension_stop` | Stop a dev/start/preview session (server + browser) |
|
|
83
93
|
| see | `extension_manifest_validate` | Cross-browser manifest validation |
|
|
84
94
|
| see | `extension_inspect` | Build output analysis |
|
|
85
95
|
| see | `extension_source_inspect` | Live DOM inspection (CDP) |
|
|
@@ -99,12 +109,22 @@ cp node_modules/@extension.dev/mcp/claude/commands/*.md ~/my-extension/.claude/c
|
|
|
99
109
|
| platform | `extension_publish` | Publish a shareable preview to extension.dev |
|
|
100
110
|
| platform | `extension_release_promote` | Promote a build to a release channel, headless |
|
|
101
111
|
|
|
102
|
-
Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension` CLI; everything else runs in-process.
|
|
112
|
+
Browser-launching tools (`dev`, `start`, `preview`) shell out to the `extension` CLI — the project's own `node_modules/.bin/extension` when present, otherwise `npx extension@<pinned>` at the version this package is verified against; everything else runs in-process.
|
|
103
113
|
|
|
104
114
|
## From preview to store
|
|
105
115
|
|
|
106
116
|
The four platform tools connect agents to [extension.dev](https://extension.dev): `extension_login` runs a GitHub device-code flow and stores a project-scoped token locally (never returned to the agent), `extension_publish` turns a build into a shareable preview URL, and `extension_release_promote` promotes a tested build to a release channel from CI or an agent session, no browser required. To automate store submission itself (Chrome Web Store, Edge Add-ons, Firefox AMO), pair with [`@extension.dev/deploy`](https://www.npmjs.com/package/@extension.dev/deploy).
|
|
107
117
|
|
|
118
|
+
## The extension.dev stack
|
|
119
|
+
|
|
120
|
+
| Package | Use it to |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| [`@extension.dev/skill`](https://www.npmjs.com/package/@extension.dev/skill) | Teach AI agents the judgment half: cross-browser rules, gotchas, playbooks |
|
|
123
|
+
| [`@extension.dev/deploy`](https://www.npmjs.com/package/@extension.dev/deploy) | Ship to Chrome, Firefox, and Edge stores from CI |
|
|
124
|
+
| [`@extension.dev/core`](https://www.npmjs.com/package/@extension.dev/core) | Authenticate and publish to the extension.dev platform |
|
|
125
|
+
|
|
126
|
+
All of it rides on [Extension.js](https://github.com/extension-js/extension.js), the open-source cross-browser extension framework.
|
|
127
|
+
|
|
108
128
|
## Community
|
|
109
129
|
|
|
110
130
|
- Join the [Discord](https://discord.gg/v9h2RgeTSN) for help and feedback
|
|
@@ -612,6 +612,47 @@ The `similarTemplates` field lists templates from the catalog with similar surfa
|
|
|
612
612
|
|
|
613
613
|
---
|
|
614
614
|
|
|
615
|
+
#### `extension_stop`
|
|
616
|
+
|
|
617
|
+
**Source:** MCP `lib/process-manager` + the `ready.json` contract (no CLI verb)
|
|
618
|
+
|
|
619
|
+
**Purpose:** Terminate a running dev/start/preview session — the dev server AND the browser it launched. The lifecycle counterpart to `extension_dev`/`extension_start`.
|
|
620
|
+
|
|
621
|
+
```json
|
|
622
|
+
{
|
|
623
|
+
"name": "extension_stop",
|
|
624
|
+
"description": "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched.",
|
|
625
|
+
"inputSchema": {
|
|
626
|
+
"type": "object",
|
|
627
|
+
"properties": {
|
|
628
|
+
"projectPath": {
|
|
629
|
+
"type": "string",
|
|
630
|
+
"description": "Path to the extension project root"
|
|
631
|
+
},
|
|
632
|
+
"browser": {
|
|
633
|
+
"type": "string",
|
|
634
|
+
"default": "chrome",
|
|
635
|
+
"description": "Browser of the session to stop"
|
|
636
|
+
},
|
|
637
|
+
"all": {
|
|
638
|
+
"type": "boolean",
|
|
639
|
+
"default": false,
|
|
640
|
+
"description": "Stop every session this server started"
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
"required": []
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**Returns:** `{ projectPath, browser, pid, stopped, detail }` (or `{ stopped: [...] }` with `all: true`).
|
|
649
|
+
|
|
650
|
+
**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.
|
|
651
|
+
|
|
652
|
+
**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.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
615
656
|
### Tier 3 — Browser management tools
|
|
616
657
|
|
|
617
658
|
#### `extension_install_browser`
|
|
@@ -721,6 +762,7 @@ The `similarTemplates` field lists templates from the catalog with similar surfa
|
|
|
721
762
|
| `extension_source_inspect` | `programs/extension` | CDP client / RDP transport | Live browser via debugging protocol | Wire to running session |
|
|
722
763
|
| `extension_list_extensions` | MCP `lib/cdp` | `Extensions.getExtensionInfo` (read-only) | Live browser via CDP (Chromium) | MCP tool (no CLI verb) |
|
|
723
764
|
| `extension_wait` | `programs/extension` | `dev-wait.ts` | `ready.json` contract file | Thin wrapper (exists in CLI) |
|
|
765
|
+
| `extension_stop` | MCP `lib/process-manager` | session registry + group signal | Session registry + `ready.json` pid | MCP tool (no CLI verb) |
|
|
724
766
|
| `extension_add_feature` | New | `extension_get_template_source` | examples repo patterns | Codegen from examples |
|
|
725
767
|
| **Agent bridge — act / triggers** | | | | |
|
|
726
768
|
| `extension_eval` | `programs/extension` | bridge control channel | Live extension context | Wraps `extension eval` (`--allow-eval`) |
|
package/dist/module.js
CHANGED
|
@@ -2,9 +2,9 @@ import { __webpack_require__ } from "./rslib-runtime.js";
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import node_path, { join } from "node:path";
|
|
5
6
|
import { extensionCreate } from "extension-create";
|
|
6
7
|
import node_fs, { existsSync, readdirSync, statSync } from "node:fs";
|
|
7
|
-
import node_path, { join } from "node:path";
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import { extensionBuild } from "extension-develop";
|
|
10
10
|
import cross_spawn from "cross-spawn";
|
|
@@ -31,7 +31,7 @@ var create_namespaceObject = {};
|
|
|
31
31
|
__webpack_require__.r(create_namespaceObject);
|
|
32
32
|
__webpack_require__.d(create_namespaceObject, {
|
|
33
33
|
handler: ()=>create_handler,
|
|
34
|
-
schema: ()=>
|
|
34
|
+
schema: ()=>create_schema
|
|
35
35
|
});
|
|
36
36
|
var detect_browsers_namespaceObject = {};
|
|
37
37
|
__webpack_require__.r(detect_browsers_namespaceObject);
|
|
@@ -159,6 +159,12 @@ __webpack_require__.d(start_namespaceObject, {
|
|
|
159
159
|
handler: ()=>start_handler,
|
|
160
160
|
schema: ()=>start_schema
|
|
161
161
|
});
|
|
162
|
+
var stop_namespaceObject = {};
|
|
163
|
+
__webpack_require__.r(stop_namespaceObject);
|
|
164
|
+
__webpack_require__.d(stop_namespaceObject, {
|
|
165
|
+
handler: ()=>stop_handler,
|
|
166
|
+
schema: ()=>stop_schema
|
|
167
|
+
});
|
|
162
168
|
var storage_namespaceObject = {};
|
|
163
169
|
__webpack_require__.r(storage_namespaceObject);
|
|
164
170
|
__webpack_require__.d(storage_namespaceObject, {
|
|
@@ -177,10 +183,8 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
177
183
|
handler: ()=>whoami_handler,
|
|
178
184
|
schema: ()=>whoami_schema
|
|
179
185
|
});
|
|
180
|
-
var package_namespaceObject = {
|
|
181
|
-
|
|
182
|
-
};
|
|
183
|
-
const schema = {
|
|
186
|
+
var package_namespaceObject = JSON.parse('{"rE":"4.2.0","El":{"OP":"^4.0.8"}}');
|
|
187
|
+
const create_schema = {
|
|
184
188
|
name: "extension_create",
|
|
185
189
|
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
186
190
|
inputSchema: {
|
|
@@ -190,6 +194,10 @@ const schema = {
|
|
|
190
194
|
type: "string",
|
|
191
195
|
description: "Name of the extension project (used as directory name)"
|
|
192
196
|
},
|
|
197
|
+
parentDir: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: "Directory to create the project inside. Defaults to the MCP server's working directory, which may not be where you expect — pass this explicitly when you care where the project lands."
|
|
200
|
+
},
|
|
193
201
|
template: {
|
|
194
202
|
type: "string",
|
|
195
203
|
default: "typescript",
|
|
@@ -208,8 +216,9 @@ const schema = {
|
|
|
208
216
|
};
|
|
209
217
|
async function create_handler(args) {
|
|
210
218
|
const start = Date.now();
|
|
219
|
+
const projectInput = args.parentDir ? node_path.resolve(args.parentDir, args.projectName) : args.projectName;
|
|
211
220
|
try {
|
|
212
|
-
const result = await extensionCreate(
|
|
221
|
+
const result = await extensionCreate(projectInput, {
|
|
213
222
|
template: args.template ?? "typescript",
|
|
214
223
|
install: args.install ?? true,
|
|
215
224
|
logger: {
|
|
@@ -224,7 +233,7 @@ async function create_handler(args) {
|
|
|
224
233
|
depsInstalled: result.depsInstalled,
|
|
225
234
|
duration: Date.now() - start,
|
|
226
235
|
nextSteps: [
|
|
227
|
-
`cd ${result.
|
|
236
|
+
`cd ${result.projectPath}`,
|
|
228
237
|
"npm run dev"
|
|
229
238
|
]
|
|
230
239
|
});
|
|
@@ -407,10 +416,30 @@ async function build_handler(args) {
|
|
|
407
416
|
});
|
|
408
417
|
}
|
|
409
418
|
}
|
|
419
|
+
const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
|
|
420
|
+
function resolveExtensionInvocation(projectDir) {
|
|
421
|
+
if (projectDir) {
|
|
422
|
+
const bin = node_path.join(projectDir, "node_modules", ".bin", "win32" === process.platform ? "extension.cmd" : "extension");
|
|
423
|
+
try {
|
|
424
|
+
node_fs.accessSync(bin, node_fs.constants.X_OK);
|
|
425
|
+
return {
|
|
426
|
+
command: bin,
|
|
427
|
+
prefixArgs: []
|
|
428
|
+
};
|
|
429
|
+
} catch {}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
command: "npx",
|
|
433
|
+
prefixArgs: [
|
|
434
|
+
`extension@${PINNED_CLI_VERSION}`
|
|
435
|
+
]
|
|
436
|
+
};
|
|
437
|
+
}
|
|
410
438
|
function runExtensionCli(args, options) {
|
|
439
|
+
const { command, prefixArgs } = resolveExtensionInvocation(options?.cwd);
|
|
411
440
|
return new Promise((resolve)=>{
|
|
412
|
-
const child = cross_spawn(
|
|
413
|
-
|
|
441
|
+
const child = cross_spawn(command, [
|
|
442
|
+
...prefixArgs,
|
|
414
443
|
...args
|
|
415
444
|
], {
|
|
416
445
|
cwd: options?.cwd,
|
|
@@ -443,8 +472,9 @@ function runExtensionCli(args, options) {
|
|
|
443
472
|
});
|
|
444
473
|
}
|
|
445
474
|
function spawnExtensionCli(args, options) {
|
|
446
|
-
const
|
|
447
|
-
|
|
475
|
+
const { command, prefixArgs } = resolveExtensionInvocation(options?.projectDir ?? options?.cwd);
|
|
476
|
+
const child = cross_spawn(command, [
|
|
477
|
+
...prefixArgs,
|
|
448
478
|
...args
|
|
449
479
|
], {
|
|
450
480
|
cwd: options?.cwd,
|
|
@@ -463,12 +493,21 @@ function spawnExtensionCli(args, options) {
|
|
|
463
493
|
child.unref();
|
|
464
494
|
return child;
|
|
465
495
|
}
|
|
466
|
-
const
|
|
496
|
+
const process_manager_sessions = new Map();
|
|
467
497
|
function sessionKey(projectPath, browser) {
|
|
468
|
-
return `${projectPath}::${browser}`;
|
|
498
|
+
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
469
499
|
}
|
|
470
500
|
function registerSession(info) {
|
|
471
|
-
|
|
501
|
+
process_manager_sessions.set(sessionKey(info.projectPath, info.browser), info);
|
|
502
|
+
}
|
|
503
|
+
function getSession(projectPath, browser) {
|
|
504
|
+
return process_manager_sessions.get(sessionKey(projectPath, browser));
|
|
505
|
+
}
|
|
506
|
+
function removeSession(projectPath, browser) {
|
|
507
|
+
process_manager_sessions.delete(sessionKey(projectPath, browser));
|
|
508
|
+
}
|
|
509
|
+
function listSessions() {
|
|
510
|
+
return Array.from(process_manager_sessions.values());
|
|
472
511
|
}
|
|
473
512
|
const dev_schema = {
|
|
474
513
|
name: "extension_dev",
|
|
@@ -528,7 +567,9 @@ async function dev_handler(args) {
|
|
|
528
567
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
529
568
|
if (args.allowControl) cliArgs.push("--allow-control");
|
|
530
569
|
if (args.allowEval) cliArgs.push("--allow-eval");
|
|
531
|
-
const child = spawnExtensionCli(cliArgs
|
|
570
|
+
const child = spawnExtensionCli(cliArgs, {
|
|
571
|
+
projectDir: args.projectPath
|
|
572
|
+
});
|
|
532
573
|
const pid = child.pid;
|
|
533
574
|
registerSession({
|
|
534
575
|
pid,
|
|
@@ -537,6 +578,7 @@ async function dev_handler(args) {
|
|
|
537
578
|
projectPath: args.projectPath,
|
|
538
579
|
command: "dev"
|
|
539
580
|
});
|
|
581
|
+
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
540
582
|
let earlyOutput = "";
|
|
541
583
|
const collector = (data)=>{
|
|
542
584
|
earlyOutput += data.toString();
|
|
@@ -552,7 +594,7 @@ async function dev_handler(args) {
|
|
|
552
594
|
port: args.port ?? 8080,
|
|
553
595
|
projectPath: args.projectPath,
|
|
554
596
|
status: "started",
|
|
555
|
-
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state.",
|
|
597
|
+
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. When you are done, call extension_stop to shut down the dev server and browser.",
|
|
556
598
|
earlyOutput: earlyOutput.slice(0, 500)
|
|
557
599
|
});
|
|
558
600
|
}
|
|
@@ -597,7 +639,9 @@ async function start_handler(args) {
|
|
|
597
639
|
browser
|
|
598
640
|
];
|
|
599
641
|
if (false === args.polyfill) cliArgs.push("--no-polyfill");
|
|
600
|
-
const child = spawnExtensionCli(cliArgs
|
|
642
|
+
const child = spawnExtensionCli(cliArgs, {
|
|
643
|
+
projectDir: args.projectPath
|
|
644
|
+
});
|
|
601
645
|
const pid = child.pid;
|
|
602
646
|
registerSession({
|
|
603
647
|
pid,
|
|
@@ -605,6 +649,7 @@ async function start_handler(args) {
|
|
|
605
649
|
projectPath: args.projectPath,
|
|
606
650
|
command: "start"
|
|
607
651
|
});
|
|
652
|
+
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
608
653
|
let earlyOutput = "";
|
|
609
654
|
const collector = (data)=>{
|
|
610
655
|
earlyOutput += data.toString();
|
|
@@ -619,7 +664,7 @@ async function start_handler(args) {
|
|
|
619
664
|
browser,
|
|
620
665
|
projectPath: args.projectPath,
|
|
621
666
|
status: "started",
|
|
622
|
-
hint: "Use extension_wait to check when the build and browser launch are complete.",
|
|
667
|
+
hint: "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session.",
|
|
623
668
|
earlyOutput: earlyOutput.slice(0, 500)
|
|
624
669
|
});
|
|
625
670
|
}
|
|
@@ -658,13 +703,134 @@ async function preview_handler(args) {
|
|
|
658
703
|
"--browser",
|
|
659
704
|
browser
|
|
660
705
|
];
|
|
661
|
-
const child = spawnExtensionCli(cliArgs
|
|
706
|
+
const child = spawnExtensionCli(cliArgs, {
|
|
707
|
+
projectDir: args.projectPath
|
|
708
|
+
});
|
|
709
|
+
const pid = child.pid;
|
|
710
|
+
registerSession({
|
|
711
|
+
pid,
|
|
712
|
+
browser,
|
|
713
|
+
projectPath: args.projectPath,
|
|
714
|
+
command: "preview"
|
|
715
|
+
});
|
|
716
|
+
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
662
717
|
return JSON.stringify({
|
|
663
|
-
pid
|
|
718
|
+
pid,
|
|
664
719
|
browser,
|
|
665
720
|
projectPath: args.projectPath,
|
|
666
|
-
status: "launched"
|
|
721
|
+
status: "launched",
|
|
722
|
+
hint: "Call extension_stop when you are done to close the preview browser."
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
const stop_schema = {
|
|
726
|
+
name: "extension_stop",
|
|
727
|
+
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
728
|
+
inputSchema: {
|
|
729
|
+
type: "object",
|
|
730
|
+
properties: {
|
|
731
|
+
projectPath: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "Path to the extension project root"
|
|
734
|
+
},
|
|
735
|
+
browser: {
|
|
736
|
+
type: "string",
|
|
737
|
+
default: "chrome",
|
|
738
|
+
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start)"
|
|
739
|
+
},
|
|
740
|
+
all: {
|
|
741
|
+
type: "boolean",
|
|
742
|
+
default: false,
|
|
743
|
+
description: "Stop every session this server started, across projects and browsers. When true, projectPath/browser are ignored."
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
required: []
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
function isAlive(pid) {
|
|
750
|
+
try {
|
|
751
|
+
process.kill(pid, 0);
|
|
752
|
+
return true;
|
|
753
|
+
} catch {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function signal(pid, sig) {
|
|
758
|
+
try {
|
|
759
|
+
process.kill(-pid, sig);
|
|
760
|
+
return true;
|
|
761
|
+
} catch {
|
|
762
|
+
try {
|
|
763
|
+
process.kill(pid, sig);
|
|
764
|
+
return true;
|
|
765
|
+
} catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function readyJsonPath(projectPath, browser) {
|
|
771
|
+
return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
772
|
+
}
|
|
773
|
+
function pidFromReadyContract(projectPath, browser) {
|
|
774
|
+
try {
|
|
775
|
+
const raw = node_fs.readFileSync(readyJsonPath(projectPath, browser), "utf8");
|
|
776
|
+
const contract = JSON.parse(raw);
|
|
777
|
+
return "number" == typeof contract.pid ? contract.pid : null;
|
|
778
|
+
} catch {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function stopOne(projectPath, browser) {
|
|
783
|
+
const session = getSession(projectPath, browser);
|
|
784
|
+
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
785
|
+
if (null == pid) return {
|
|
786
|
+
projectPath,
|
|
787
|
+
browser,
|
|
788
|
+
pid: null,
|
|
789
|
+
stopped: false,
|
|
790
|
+
detail: "No known session for this project/browser (nothing registered in this server and no ready.json contract found)."
|
|
791
|
+
};
|
|
792
|
+
let detail;
|
|
793
|
+
if (isAlive(pid)) {
|
|
794
|
+
signal(pid, "SIGTERM");
|
|
795
|
+
await new Promise((resolve)=>setTimeout(resolve, 1500));
|
|
796
|
+
if (isAlive(pid)) {
|
|
797
|
+
signal(pid, "SIGKILL");
|
|
798
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
799
|
+
}
|
|
800
|
+
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
801
|
+
} else detail = "Process was already gone; cleaned up session records.";
|
|
802
|
+
removeSession(projectPath, browser);
|
|
803
|
+
try {
|
|
804
|
+
node_fs.rmSync(readyJsonPath(projectPath, browser), {
|
|
805
|
+
force: true
|
|
806
|
+
});
|
|
807
|
+
} catch {}
|
|
808
|
+
return {
|
|
809
|
+
projectPath,
|
|
810
|
+
browser,
|
|
811
|
+
pid,
|
|
812
|
+
stopped: !isAlive(pid),
|
|
813
|
+
detail
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async function stop_handler(args) {
|
|
817
|
+
if (args.all) {
|
|
818
|
+
const sessions = listSessions();
|
|
819
|
+
if (0 === sessions.length) return JSON.stringify({
|
|
820
|
+
stopped: [],
|
|
821
|
+
message: "No sessions registered in this server."
|
|
822
|
+
});
|
|
823
|
+
const outcomes = [];
|
|
824
|
+
for (const s of sessions)outcomes.push(await stopOne(s.projectPath, s.browser));
|
|
825
|
+
return JSON.stringify({
|
|
826
|
+
stopped: outcomes
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
if (!args.projectPath) return JSON.stringify({
|
|
830
|
+
error: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
|
|
667
831
|
});
|
|
832
|
+
const outcome = await stopOne(args.projectPath, args.browser ?? "chrome");
|
|
833
|
+
return JSON.stringify(outcome);
|
|
668
834
|
}
|
|
669
835
|
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
670
836
|
const get_template_source_schema = {
|
|
@@ -3269,6 +3435,70 @@ async function detect_browsers_handler(args) {
|
|
|
3269
3435
|
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}. Use extension_install_browser to install them.` : "All requested browsers are available."
|
|
3270
3436
|
});
|
|
3271
3437
|
}
|
|
3438
|
+
function typeOf(value) {
|
|
3439
|
+
if (Array.isArray(value)) return "array";
|
|
3440
|
+
if (null === value) return "null";
|
|
3441
|
+
return typeof value;
|
|
3442
|
+
}
|
|
3443
|
+
function checkPrimitive(path, value, schema, issues) {
|
|
3444
|
+
if (schema.type && [
|
|
3445
|
+
"string",
|
|
3446
|
+
"number",
|
|
3447
|
+
"boolean"
|
|
3448
|
+
].includes(schema.type)) {
|
|
3449
|
+
if (typeOf(value) !== schema.type) return void issues.push({
|
|
3450
|
+
path,
|
|
3451
|
+
message: `expected ${schema.type}, got ${typeOf(value)}`
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
if ("array" === schema.type) {
|
|
3455
|
+
if (!Array.isArray(value)) return void issues.push({
|
|
3456
|
+
path,
|
|
3457
|
+
message: `expected array, got ${typeOf(value)}`
|
|
3458
|
+
});
|
|
3459
|
+
const itemSchema = schema.items;
|
|
3460
|
+
if (itemSchema) value.forEach((item, i)=>checkPrimitive(`${path}[${i}]`, item, itemSchema, issues));
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
if (Array.isArray(schema.enum) && schema.enum.length) {
|
|
3464
|
+
if (!schema.enum.includes(value)) issues.push({
|
|
3465
|
+
path,
|
|
3466
|
+
message: `must be one of: ${schema.enum.map(String).join(", ")}`
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function validateToolInput(inputSchema, args) {
|
|
3471
|
+
const schema = inputSchema;
|
|
3472
|
+
const issues = [];
|
|
3473
|
+
const properties = schema.properties ?? {};
|
|
3474
|
+
for (const key of schema.required ?? [])if (void 0 === args[key]) issues.push({
|
|
3475
|
+
path: key,
|
|
3476
|
+
message: "required argument is missing"
|
|
3477
|
+
});
|
|
3478
|
+
for (const [key, value] of Object.entries(args)){
|
|
3479
|
+
const propSchema = properties[key];
|
|
3480
|
+
if (!propSchema) {
|
|
3481
|
+
const known = Object.keys(properties);
|
|
3482
|
+
issues.push({
|
|
3483
|
+
path: key,
|
|
3484
|
+
message: `unknown argument${known.length ? ` (known: ${known.join(", ")})` : ""}`
|
|
3485
|
+
});
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
if (void 0 !== value) checkPrimitive(key, value, propSchema, issues);
|
|
3489
|
+
}
|
|
3490
|
+
return issues;
|
|
3491
|
+
}
|
|
3492
|
+
function inputValidationError(toolName, issues) {
|
|
3493
|
+
return JSON.stringify({
|
|
3494
|
+
ok: false,
|
|
3495
|
+
error: {
|
|
3496
|
+
name: "InputValidationError",
|
|
3497
|
+
message: `Invalid arguments for ${toolName}: ${issues.map((i)=>`${i.path}: ${i.message}`).join("; ")}`,
|
|
3498
|
+
issues
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3272
3502
|
const tools = [
|
|
3273
3503
|
create_namespaceObject,
|
|
3274
3504
|
list_templates_namespaceObject,
|
|
@@ -3276,6 +3506,7 @@ const tools = [
|
|
|
3276
3506
|
dev_namespaceObject,
|
|
3277
3507
|
start_namespaceObject,
|
|
3278
3508
|
preview_namespaceObject,
|
|
3509
|
+
stop_namespaceObject,
|
|
3279
3510
|
get_template_source_namespaceObject,
|
|
3280
3511
|
manifest_validate_namespaceObject,
|
|
3281
3512
|
inspect_namespaceObject,
|
|
@@ -3331,6 +3562,16 @@ async function startServer() {
|
|
|
3331
3562
|
],
|
|
3332
3563
|
isError: true
|
|
3333
3564
|
};
|
|
3565
|
+
const issues = validateToolInput(tool.schema.inputSchema, args ?? {});
|
|
3566
|
+
if (issues.length) return {
|
|
3567
|
+
content: [
|
|
3568
|
+
{
|
|
3569
|
+
type: "text",
|
|
3570
|
+
text: inputValidationError(name, issues)
|
|
3571
|
+
}
|
|
3572
|
+
],
|
|
3573
|
+
isError: true
|
|
3574
|
+
};
|
|
3334
3575
|
try {
|
|
3335
3576
|
const result = await tool.handler(args ?? {});
|
|
3336
3577
|
return {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export interface ToolModule {
|
|
2
|
+
schema: {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
handler: (args: any) => Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const tools: ToolModule[];
|
|
1
10
|
export declare function startServer(): Promise<void>;
|
|
2
11
|
/**
|
|
3
12
|
* Human-facing subcommands for `extension-mcp <cmd>`. Unlike the MCP tools
|
package/dist/src/lib/exec.d.ts
CHANGED
|
@@ -4,10 +4,26 @@ export interface CliResult {
|
|
|
4
4
|
stdout: string;
|
|
5
5
|
stderr: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve how to invoke the `extension` CLI for a given project.
|
|
9
|
+
*
|
|
10
|
+
* Preference order:
|
|
11
|
+
* 1. The project's own `node_modules/.bin/extension` — the version the project
|
|
12
|
+
* pinned is the single source of behavior for that project (lockstep
|
|
13
|
+
* invariant), and spawning it needs no network.
|
|
14
|
+
* 2. `npx extension@<pinned>` — pinned to the extension-develop version this
|
|
15
|
+
* package vendors, never a floating `latest`, so MCP behavior stays
|
|
16
|
+
* reproducible even without a project-local install.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveExtensionInvocation(projectDir?: string): {
|
|
19
|
+
command: string;
|
|
20
|
+
prefixArgs: string[];
|
|
21
|
+
};
|
|
7
22
|
export declare function runExtensionCli(args: string[], options?: {
|
|
8
23
|
cwd?: string;
|
|
9
24
|
timeoutMs?: number;
|
|
10
25
|
}): Promise<CliResult>;
|
|
11
26
|
export declare function spawnExtensionCli(args: string[], options?: {
|
|
12
27
|
cwd?: string;
|
|
28
|
+
projectDir?: string;
|
|
13
29
|
}): ChildProcess;
|
package/dist/src/lib/types.d.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface InputIssue {
|
|
2
|
+
path: string;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Validate tools/call arguments against a tool inputSchema. Returns an empty
|
|
7
|
+
* array when the args are acceptable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateToolInput(inputSchema: Record<string, unknown>, args: Record<string, unknown>): InputIssue[];
|
|
10
|
+
/** Frozen-style error envelope for input validation failures. */
|
|
11
|
+
export declare function inputValidationError(toolName: string, issues: InputIssue[]): string;
|
|
@@ -8,6 +8,10 @@ export declare const schema: {
|
|
|
8
8
|
type: string;
|
|
9
9
|
description: string;
|
|
10
10
|
};
|
|
11
|
+
parentDir: {
|
|
12
|
+
type: string;
|
|
13
|
+
description: string;
|
|
14
|
+
};
|
|
11
15
|
template: {
|
|
12
16
|
type: string;
|
|
13
17
|
default: string;
|
|
@@ -24,6 +28,7 @@ export declare const schema: {
|
|
|
24
28
|
};
|
|
25
29
|
export declare function handler(args: {
|
|
26
30
|
projectName: string;
|
|
31
|
+
parentDir?: string;
|
|
27
32
|
template?: string;
|
|
28
33
|
install?: boolean;
|
|
29
34
|
}): Promise<string>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const schema: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object";
|
|
6
|
+
properties: {
|
|
7
|
+
projectPath: {
|
|
8
|
+
type: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
browser: {
|
|
12
|
+
type: string;
|
|
13
|
+
default: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
all: {
|
|
17
|
+
type: string;
|
|
18
|
+
default: boolean;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: never[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare function handler(args: {
|
|
26
|
+
projectPath?: string;
|
|
27
|
+
browser?: string;
|
|
28
|
+
all?: boolean;
|
|
29
|
+
}): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
5
|
-
"description": "MCP server that lets AI agents build, run, inspect, and publish browser extensions.
|
|
4
|
+
"version": "4.2.0",
|
|
5
|
+
"description": "MCP server that lets AI agents build, run, inspect, and publish browser extensions. 28 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, and Firefox.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Cezar Augusto",
|