@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 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. 27 MCP tools that scaffold, run, inspect, debug, and publish cross-browser extensions.
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: ()=>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
- rE: "4.1.1"
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(args.projectName, {
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.projectName}`,
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("npx", [
413
- "extension",
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 child = cross_spawn("npx", [
447
- "extension",
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 sessions = new Map();
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
- sessions.set(sessionKey(info.projectPath, info.browser), info);
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: child.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 {
@@ -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
@@ -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;
@@ -63,6 +63,6 @@ export interface ProcessInfo {
63
63
  browser: string;
64
64
  port?: number;
65
65
  projectPath: string;
66
- command: "dev" | "start";
66
+ command: "dev" | "start" | "preview";
67
67
  }
68
68
  export type BrowserType = "chrome" | "edge" | "firefox" | "chromium-based" | "gecko-based";
@@ -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.1.1",
5
- "description": "MCP server that lets AI agents build, run, inspect, and publish browser extensions. 27 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, and Firefox.",
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",