[react-devtools-cdt-mcp] Add explicit register entry (#36972)
We should provide a way for consumers to handle the installation on their own, so the index entrypoint will just export the APIs and won't have any side-effects. The `/register` entrypoint (via `import react-devtools-cdt-mcp/register`), will subscribe an event listener and install the tools.
Ruslan Lesiutin committed
Jul 16, 2026 at 18:15 UTC
f598ec1c29c98c368e0a576d4e0cf58831038140
8 files changed
+148
-66
packages/react-devtools-cdt-mcp/README.md
+15
-7
@@ -3,21 +3,29 @@
3
Integrates React tools with
4
[chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp).
5
6
-Importing this package **before React** installs the DevTools hook and registers
7
-a React tool group via chrome-devtools-mcp's `devtoolstooldiscovery` / `__dtmcp`
8
-third-party-tool protocol. The React tools then become discoverable and callable
9
-inside a chrome-devtools-mcp session — no separate server.
6
+Importing `react-devtools-cdt-mcp/register` **before React** installs the
7
+DevTools hook and registers a React tool group via chrome-devtools-mcp's
8
+`devtoolstooldiscovery` / `__dtmcp` third-party-tool protocol. The React tools
9
+then become discoverable and callable inside a chrome-devtools-mcp session — no
10
+separate server.
11
12
## Usage
13
13
-Import the package **before** React so the hook is installed before React
14
-initializes:
14
+Import the register entry **before** React so the hook is installed before
15
+React initializes:
16
17
```js
17
-import 'react-devtools-cdt-mcp';
18
+import 'react-devtools-cdt-mcp/register';
19
import React from 'react';
20
```
21
22
+The package root is side-effect-free and exports the lower-level API for custom
23
+targets:
24
+
25
+```js
26
+import {register, buildToolGroup} from 'react-devtools-cdt-mcp';
27
+```
28
+
29
When the page runs under chrome-devtools-mcp, the React tools are listed by
30
`list_3p_developer_tools` and callable either via
31
`execute_3p_developer_tool({toolName, params})` or directly via `evaluate_script`
packages/react-devtools-cdt-mcp/fixtures/app/index.js
+4
-4
@@ -1,7 +1,7 @@
1
-// Import the package source first so it installs the DevTools hook and registers
2
-// the chrome-devtools-mcp tool group BEFORE react-dom evaluates — the hook must
3
-// be in place when React's renderer injects on the first commit.
4
-import '../../src/index.js';
1
+// Import the register entry first so it installs the DevTools hook and
2
+// registers the chrome-devtools-mcp tool group BEFORE react-dom evaluates — the
3
+// hook must be in place when React's renderer injects on the first commit.
4
+import '../../src/register.js';
5
6
import * as React from 'react';
7
import {createRoot} from 'react-dom/client';
packages/react-devtools-cdt-mcp/package.json
+2
-1
@@ -11,7 +11,8 @@
11
},
12
"files": [
13
"dist",
14
- "index.js"
14
+ "index.js",
15
+ "register.js"
16
],
17
"scripts": {
18
"build": "cross-env NODE_ENV=production rollup -c rollup.config.cjs",
packages/react-devtools-cdt-mcp/register.js
new
+3
@@ -0,0 +1,3 @@
1
+'use strict';
2
+
3
+module.exports = require('./dist/register.js');
packages/react-devtools-cdt-mcp/rollup.config.cjs
+41
-32
@@ -9,35 +9,44 @@ if (!NODE_ENV) {
9
process.exit(1);
10
}
11
12
-module.exports = {
13
- input: 'src/index.js',
14
- // CommonJS bundle for the npm package (referenced by the root index.js stub).
15
- output: {
16
- file: 'dist/index.js',
17
- format: 'cjs',
18
- exports: 'named',
19
- },
20
- treeshake: {moduleSideEffects: false},
21
- plugins: [
22
- nodeResolve({
23
- extensions: ['.js', '.mjs'],
24
- }),
25
- commonjs({
26
- include: /node_modules/,
27
- }),
28
- babel({
29
- configFile: __dirname + '/../react-devtools-shared/babel.config.js',
30
- babelHelpers: 'bundled',
31
- }),
32
- replace({
33
- preventAssignment: true,
34
- values: {
35
- __DEV__: String(NODE_ENV === 'development'),
36
- __IS_CHROME__: 'false',
37
- __IS_FIREFOX__: 'false',
38
- __IS_EDGE__: 'false',
39
- __IS_NATIVE__: 'false',
40
- },
41
- }),
42
- ],
43
-};
12
+const plugins = [
13
+ nodeResolve({
14
+ extensions: ['.js', '.mjs'],
15
+ }),
16
+ commonjs({
17
+ include: /node_modules/,
18
+ }),
19
+ babel({
20
+ configFile: __dirname + '/../react-devtools-shared/babel.config.js',
21
+ babelHelpers: 'bundled',
22
+ }),
23
+ replace({
24
+ preventAssignment: true,
25
+ values: {
26
+ __DEV__: String(NODE_ENV === 'development'),
27
+ __IS_CHROME__: 'false',
28
+ __IS_FIREFOX__: 'false',
29
+ __IS_EDGE__: 'false',
30
+ __IS_NATIVE__: 'false',
31
+ },
32
+ }),
33
+];
34
+
35
+function createBundle(input, file) {
36
+ return {
37
+ input,
38
+ // CommonJS bundles for the npm package (referenced by root stubs).
39
+ output: {
40
+ file,
41
+ format: 'cjs',
42
+ exports: 'named',
43
+ },
44
+ treeshake: {moduleSideEffects: false},
45
+ plugins,
46
+ };
47
+}
48
+
49
+module.exports = [
50
+ createBundle('src/index.js', 'dist/index.js'),
51
+ createBundle('src/register.js', 'dist/register.js'),
52
+];
packages/react-devtools-cdt-mcp/src/__tests__/DevToolsCdtMcp-test.js
+54
-3
@@ -94,7 +94,19 @@ describe('react-devtools-cdt-mcp', () => {
94
expect(globalThis.__dtmcp).toBeUndefined();
95
});
96
97
- it('throws when the auto entry is imported outside an event target', () => {
97
+ it('root entry exports tools without registering', () => {
98
+ unregister();
99
+ delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
100
+ jest.resetModules();
101
+
102
+ const api = require('../index');
103
+
104
+ expect(typeof api.register).toBe('function');
105
+ expect(typeof api.buildToolGroup).toBe('function');
106
+ expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBeUndefined();
107
+ });
108
+
109
+ it('throws when the register entry is imported outside an event target', () => {
110
const originalAddEventListener = globalThis.addEventListener;
111
const originalRemoveEventListener = globalThis.removeEventListener;
112
@@ -108,8 +120,8 @@ describe('react-devtools-cdt-mcp', () => {
120
// $FlowFixMe[cannot-write]
121
globalThis.removeEventListener = undefined;
122
111
- expect(() => require('../index')).toThrow(
112
- 'react-devtools-cdt-mcp must be imported in a browser-like environment',
123
+ expect(() => require('../register')).toThrow(
124
+ 'react-devtools-cdt-mcp/register must be imported in a browser-like environment',
125
);
126
expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBeUndefined();
127
} finally {
@@ -118,6 +130,45 @@ describe('react-devtools-cdt-mcp', () => {
130
}
131
});
132
133
+ it('register entry installs the DevTools hook', () => {
134
+ const originalAddEventListener = globalThis.addEventListener;
135
+ const originalRemoveEventListener = globalThis.removeEventListener;
136
+ let autoListener = null;
137
+
138
+ unregister();
139
+ delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
140
+ jest.resetModules();
141
+
142
+ try {
143
+ // $FlowFixMe[cannot-write]
144
+ globalThis.addEventListener = (type, listener, options) => {
145
+ if (type === 'devtoolstooldiscovery') {
146
+ autoListener = listener;
147
+ }
148
+ return originalAddEventListener.call(
149
+ globalThis,
150
+ type,
151
+ listener,
152
+ options,
153
+ );
154
+ };
155
+
156
+ require('../register');
157
+
158
+ expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBeDefined();
159
+ } finally {
160
+ if (autoListener !== null) {
161
+ originalRemoveEventListener.call(
162
+ globalThis,
163
+ 'devtoolstooldiscovery',
164
+ autoListener,
165
+ );
166
+ }
167
+ globalThis.addEventListener = originalAddEventListener;
168
+ globalThis.removeEventListener = originalRemoveEventListener;
169
+ }
170
+ });
171
+
172
it('returns the cached registration for repeated calls per target', () => {
173
let listener = null;
174
const target = {
packages/react-devtools-cdt-mcp/src/index.js
-19
@@ -7,23 +7,4 @@
7
* @flow
8
*/
9
10
-import {register} from './DevToolsCdtMcp';
11
-
12
-// Side effect: install the facade (before React) and register the React tool
13
-// group for chrome-devtools-mcp. Import this module before React.
14
-if (
15
- typeof window !== 'undefined' &&
16
- typeof window.addEventListener === 'function' &&
17
- typeof window.removeEventListener === 'function'
18
-) {
19
- register();
20
-} else {
21
- // eslint-disable-next-line react-internal/prod-error-codes
22
- throw new Error(
23
- 'react-devtools-cdt-mcp must be imported in a browser-like environment ' +
24
- 'before React initializes. Use a client-only entry point, or the manual ' +
25
- 'entry point for custom targets.',
26
- );
27
-}
28
-
10
export * from './DevToolsCdtMcp';
packages/react-devtools-cdt-mcp/src/register.js
new
+29
@@ -0,0 +1,29 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {register} from './DevToolsCdtMcp';
11
+
12
+// Side effect: install the facade (before React) and register the React tool
13
+// group for chrome-devtools-mcp. Import this module before React.
14
+if (
15
+ typeof window !== 'undefined' &&
16
+ typeof window.addEventListener === 'function' &&
17
+ typeof window.removeEventListener === 'function'
18
+) {
19
+ register();
20
+} else {
21
+ // eslint-disable-next-line react-internal/prod-error-codes
22
+ throw new Error(
23
+ 'react-devtools-cdt-mcp/register must be imported in a browser-like ' +
24
+ 'environment before React initializes. Use the root entry point for ' +
25
+ 'side-effect-free access to register().',
26
+ );
27
+}
28
+
29
+export * from './DevToolsCdtMcp';