react-devtools-bridge 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,2806 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema,
9
+ ListResourcesRequestSchema,
10
+ ReadResourceRequestSchema
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+
13
+ // src/bridge.ts
14
+ import { WebSocket } from "ws";
15
+ import { EventEmitter } from "events";
16
+
17
+ // src/logger.ts
18
+ var LOG_LEVELS = {
19
+ debug: 0,
20
+ info: 1,
21
+ warn: 2,
22
+ error: 3,
23
+ silent: 4
24
+ };
25
+ function defaultOutput(entry) {
26
+ const timestamp = new Date(entry.timestamp).toISOString();
27
+ const prefix = entry.prefix ? `[${entry.prefix}]` : "";
28
+ const level = entry.level.toUpperCase().padEnd(5);
29
+ const meta = entry.meta ? ` ${JSON.stringify(entry.meta)}` : "";
30
+ console.error(`${timestamp} ${level} ${prefix} ${entry.message}${meta}`);
31
+ }
32
+ function createLogger(options = {}) {
33
+ const level = options.level ?? "warn";
34
+ const prefix = options.prefix;
35
+ const output = options.output ?? defaultOutput;
36
+ const minLevel = LOG_LEVELS[level];
37
+ const log = (logLevel, message, meta) => {
38
+ if (LOG_LEVELS[logLevel] < minLevel) return;
39
+ output({
40
+ level: logLevel,
41
+ message,
42
+ timestamp: Date.now(),
43
+ prefix,
44
+ meta
45
+ });
46
+ };
47
+ return {
48
+ debug: (message, meta) => log("debug", message, meta),
49
+ info: (message, meta) => log("info", message, meta),
50
+ warn: (message, meta) => log("warn", message, meta),
51
+ error: (message, meta) => log("error", message, meta),
52
+ child: (childPrefix) => createLogger({
53
+ level,
54
+ prefix: prefix ? `${prefix}:${childPrefix}` : childPrefix,
55
+ output
56
+ })
57
+ };
58
+ }
59
+ var noopLogger = {
60
+ debug: () => {
61
+ },
62
+ info: () => {
63
+ },
64
+ warn: () => {
65
+ },
66
+ error: () => {
67
+ },
68
+ child: () => noopLogger
69
+ };
70
+ function getLogLevelFromEnv() {
71
+ const envLevel = process.env.DEVTOOLS_LOG_LEVEL?.toLowerCase();
72
+ if (envLevel && envLevel in LOG_LEVELS) {
73
+ return envLevel;
74
+ }
75
+ return process.env.DEVTOOLS_DEBUG === "true" ? "debug" : "warn";
76
+ }
77
+
78
+ // src/errors.ts
79
+ var DevToolsError = class extends Error {
80
+ constructor(message, code, details) {
81
+ super(message);
82
+ this.code = code;
83
+ this.details = details;
84
+ this.name = "DevToolsError";
85
+ Error.captureStackTrace?.(this, this.constructor);
86
+ }
87
+ toJSON() {
88
+ return {
89
+ name: this.name,
90
+ code: this.code,
91
+ message: this.message,
92
+ details: this.details
93
+ };
94
+ }
95
+ };
96
+ var ConnectionError = class extends DevToolsError {
97
+ constructor(message, details) {
98
+ super(message, "NOT_CONNECTED", details);
99
+ this.name = "ConnectionError";
100
+ }
101
+ };
102
+ var TimeoutError = class extends DevToolsError {
103
+ constructor(operation, timeout, details) {
104
+ super(`Request timeout after ${timeout}ms: ${operation}`, "TIMEOUT", {
105
+ operation,
106
+ timeout,
107
+ ...details
108
+ });
109
+ this.name = "TimeoutError";
110
+ }
111
+ };
112
+
113
+ // src/bridge.ts
114
+ var ELEMENT_TYPE_MAP = {
115
+ 1: "class",
116
+ 2: "context",
117
+ 5: "function",
118
+ 6: "forward_ref",
119
+ 7: "fragment",
120
+ 8: "host",
121
+ 9: "memo",
122
+ 10: "portal",
123
+ 11: "root",
124
+ 12: "profiler",
125
+ 13: "suspense",
126
+ 14: "lazy",
127
+ 15: "cache",
128
+ 16: "activity",
129
+ 17: "virtual"
130
+ };
131
+ var TREE_OP = {
132
+ ADD: 1,
133
+ REMOVE: 2,
134
+ REORDER: 3,
135
+ UPDATE_TREE_BASE_DURATION: 4,
136
+ UPDATE_ERRORS_OR_WARNINGS: 5
137
+ };
138
+ var DEFAULT_CONFIG = {
139
+ host: "localhost",
140
+ port: 8097,
141
+ timeout: 5e3,
142
+ autoReconnect: true
143
+ };
144
+ var RECONNECT = {
145
+ MAX_ATTEMPTS: 5,
146
+ BASE_DELAY: 1e3,
147
+ MAX_DELAY: 3e4
148
+ };
149
+ var DEFAULT_CAPABILITIES = {
150
+ bridgeProtocolVersion: 2,
151
+ backendVersion: null,
152
+ supportsInspectElementPaths: false,
153
+ supportsProfilingChangeDescriptions: false,
154
+ supportsTimeline: false,
155
+ supportsNativeStyleEditor: false,
156
+ supportsErrorBoundaryTesting: false,
157
+ supportsTraceUpdates: false,
158
+ isBackendStorageAPISupported: false,
159
+ isSynchronousXHRSupported: false
160
+ };
161
+ var DevToolsBridge = class extends EventEmitter {
162
+ config;
163
+ logger;
164
+ ws = null;
165
+ state = "disconnected";
166
+ error = null;
167
+ // Connection management (Phase 1.2: Race condition fix)
168
+ connectPromise = null;
169
+ // Reconnection state (Phase 1.3: Auto-reconnection)
170
+ reconnectAttempts = 0;
171
+ reconnectTimer = null;
172
+ manualDisconnect = false;
173
+ // Component tree state
174
+ elements = /* @__PURE__ */ new Map();
175
+ rootIDs = /* @__PURE__ */ new Set();
176
+ renderers = /* @__PURE__ */ new Map();
177
+ elementToRenderer = /* @__PURE__ */ new Map();
178
+ // Phase 2.3: Element-to-renderer mapping
179
+ // Request tracking (Phase 1.5 & 1.6: Memory leak fix + ID correlation)
180
+ pendingRequests = /* @__PURE__ */ new Map();
181
+ requestIdCounter = 0;
182
+ staleRequestCleanupTimer = null;
183
+ /**
184
+ * Unified fallback key mapping for request/response correlation.
185
+ * Maps element-based keys to requestID-based keys.
186
+ *
187
+ * Flow:
188
+ * 1. Request sent with requestID=123 for elementID=456
189
+ * 2. Store mapping: "inspect_456" -> "inspect_123"
190
+ * 3. Response arrives with responseID=123 OR just id=456
191
+ * 4. Try "inspect_123" first, fall back to mapping["inspect_456"]
192
+ * 5. Clean up mapping after resolving
193
+ *
194
+ * Needed because some React DevTools backends don't echo responseID reliably.
195
+ */
196
+ responseFallbackKeys = /* @__PURE__ */ new Map();
197
+ // Errors/warnings state
198
+ elementErrors = /* @__PURE__ */ new Map();
199
+ elementWarnings = /* @__PURE__ */ new Map();
200
+ // Profiling state
201
+ isProfiling = false;
202
+ profilingData = null;
203
+ // Protocol info (Phase 2.2)
204
+ backendVersion = null;
205
+ capabilities = { ...DEFAULT_CAPABILITIES };
206
+ capabilitiesNegotiated = false;
207
+ lastMessageAt = 0;
208
+ // Native inspection state (Phase 2.1)
209
+ isInspectingNative = false;
210
+ // External communication (for headless server integration)
211
+ externalSendFn = null;
212
+ isExternallyAttached = false;
213
+ constructor(options = {}) {
214
+ super();
215
+ this.config = { ...DEFAULT_CONFIG, ...options };
216
+ this.logger = options.logger ?? noopLogger;
217
+ }
218
+ /**
219
+ * Attach to an external message source (e.g., HeadlessDevToolsServer).
220
+ * When attached, the bridge receives messages from the external source
221
+ * instead of connecting via WebSocket.
222
+ */
223
+ attachToExternal(sendFn, onDetach) {
224
+ this.logger.info("Attaching to external message source");
225
+ this.externalSendFn = sendFn;
226
+ this.isExternallyAttached = true;
227
+ this.setState("connected");
228
+ this.error = null;
229
+ this.lastMessageAt = Date.now();
230
+ this.startStaleRequestCleanup();
231
+ this.send("bridge", { version: 2 });
232
+ this.negotiateCapabilities();
233
+ this.emit("connected");
234
+ return {
235
+ receiveMessage: (data) => {
236
+ this.handleMessage(data);
237
+ },
238
+ detach: () => {
239
+ this.logger.info("Detaching from external message source");
240
+ this.externalSendFn = null;
241
+ this.isExternallyAttached = false;
242
+ this.setState("disconnected");
243
+ this.reset();
244
+ onDetach?.();
245
+ }
246
+ };
247
+ }
248
+ /**
249
+ * Check if bridge is attached to an external source
250
+ */
251
+ isAttachedExternally() {
252
+ return this.isExternallyAttached;
253
+ }
254
+ // ═══════════════════════════════════════════════════════════════════════════
255
+ // CONNECTION MANAGEMENT
256
+ // ═══════════════════════════════════════════════════════════════════════════
257
+ /**
258
+ * Connect to DevTools backend.
259
+ * Handles deduplication of concurrent connect calls (Phase 1.2).
260
+ */
261
+ async connect() {
262
+ if (this.isExternallyAttached) {
263
+ this.logger.debug("Already attached externally, skipping WebSocket connect");
264
+ return this.getStatus();
265
+ }
266
+ if (this.connectPromise) {
267
+ this.logger.debug("Returning existing connection attempt");
268
+ return this.connectPromise;
269
+ }
270
+ if (this.state === "connected" && this.ws?.readyState === WebSocket.OPEN) {
271
+ this.logger.debug("Already connected");
272
+ return this.getStatus();
273
+ }
274
+ if (this.ws) {
275
+ this.logger.debug("Cleaning up stale WebSocket");
276
+ this.ws.removeAllListeners();
277
+ this.ws.close();
278
+ this.ws = null;
279
+ }
280
+ this.manualDisconnect = false;
281
+ this.connectPromise = this.doConnect();
282
+ try {
283
+ return await this.connectPromise;
284
+ } finally {
285
+ this.connectPromise = null;
286
+ }
287
+ }
288
+ /**
289
+ * Internal connection logic
290
+ */
291
+ async doConnect() {
292
+ this.setState("connecting");
293
+ const url = `ws://${this.config.host}:${this.config.port}`;
294
+ this.logger.info("Connecting to DevTools", { url });
295
+ return new Promise((resolve, reject) => {
296
+ const connectionTimeout = setTimeout(() => {
297
+ this.logger.error("Connection timeout", { url, timeout: this.config.timeout });
298
+ this.ws?.close();
299
+ this.setError("Connection timeout");
300
+ reject(new ConnectionError("Connection timeout", { url, timeout: this.config.timeout }));
301
+ }, this.config.timeout);
302
+ try {
303
+ this.ws = new WebSocket(url);
304
+ this.ws.on("open", () => {
305
+ clearTimeout(connectionTimeout);
306
+ this.logger.info("Connected to DevTools");
307
+ this.onConnected();
308
+ resolve(this.getStatus());
309
+ });
310
+ this.ws.on("message", (data) => {
311
+ this.handleMessage(data.toString());
312
+ });
313
+ this.ws.on("close", (code, reason) => {
314
+ this.handleClose(code, reason.toString());
315
+ });
316
+ this.ws.on("error", (err) => {
317
+ clearTimeout(connectionTimeout);
318
+ this.logger.error("WebSocket error", { error: err.message });
319
+ this.setError(err.message);
320
+ reject(new ConnectionError(err.message));
321
+ });
322
+ } catch (err) {
323
+ clearTimeout(connectionTimeout);
324
+ const message = err instanceof Error ? err.message : "Unknown error";
325
+ this.logger.error("Connection failed", { error: message });
326
+ this.setError(message);
327
+ reject(new ConnectionError(message));
328
+ }
329
+ });
330
+ }
331
+ /**
332
+ * Called when connection is established
333
+ */
334
+ onConnected() {
335
+ this.setState("connected");
336
+ this.error = null;
337
+ this.reconnectAttempts = 0;
338
+ this.lastMessageAt = Date.now();
339
+ if (this.reconnectTimer) {
340
+ clearTimeout(this.reconnectTimer);
341
+ this.reconnectTimer = null;
342
+ }
343
+ this.startStaleRequestCleanup();
344
+ this.send("bridge", { version: 2 });
345
+ this.negotiateCapabilities();
346
+ this.emit("connected");
347
+ }
348
+ /**
349
+ * Negotiate protocol capabilities with backend (Phase 2.2)
350
+ */
351
+ negotiateCapabilities() {
352
+ this.logger.debug("Negotiating protocol capabilities");
353
+ this.send("isBackendStorageAPISupported", {});
354
+ this.send("isSynchronousXHRSupported", {});
355
+ this.send("getSupportedRendererInterfaces", {});
356
+ }
357
+ /**
358
+ * Handle WebSocket close event
359
+ */
360
+ handleClose(code, reason) {
361
+ this.logger.info("Connection closed", { code, reason });
362
+ this.setState("disconnected");
363
+ this.emit("disconnected", { code, reason });
364
+ this.stopStaleRequestCleanup();
365
+ for (const [, req] of this.pendingRequests) {
366
+ clearTimeout(req.timeout);
367
+ req.reject(new ConnectionError("Connection closed"));
368
+ }
369
+ this.pendingRequests.clear();
370
+ if (!this.manualDisconnect && this.config.autoReconnect && code !== 1e3 && code !== 1001) {
371
+ this.scheduleReconnect();
372
+ }
373
+ }
374
+ /**
375
+ * Schedule a reconnection attempt with exponential backoff (Phase 1.3)
376
+ */
377
+ scheduleReconnect() {
378
+ if (this.reconnectAttempts >= RECONNECT.MAX_ATTEMPTS) {
379
+ this.logger.error("Max reconnection attempts reached", { attempts: this.reconnectAttempts });
380
+ this.emit("reconnectFailed", { attempts: this.reconnectAttempts });
381
+ return;
382
+ }
383
+ const delay = Math.min(
384
+ RECONNECT.BASE_DELAY * Math.pow(2, this.reconnectAttempts) + Math.random() * 1e3,
385
+ RECONNECT.MAX_DELAY
386
+ );
387
+ this.reconnectAttempts++;
388
+ this.logger.info("Scheduling reconnection", { attempt: this.reconnectAttempts, delay });
389
+ this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
390
+ this.reconnectTimer = setTimeout(() => {
391
+ this.connect().catch((err) => {
392
+ this.logger.warn("Reconnection failed", { error: err.message });
393
+ });
394
+ }, delay);
395
+ }
396
+ /**
397
+ * Disconnect from DevTools backend
398
+ */
399
+ disconnect() {
400
+ this.logger.info("Disconnecting");
401
+ this.manualDisconnect = true;
402
+ if (this.reconnectTimer) {
403
+ clearTimeout(this.reconnectTimer);
404
+ this.reconnectTimer = null;
405
+ }
406
+ if (this.ws) {
407
+ this.ws.close(1e3, "Client disconnect");
408
+ this.ws = null;
409
+ }
410
+ this.setState("disconnected");
411
+ this.reset();
412
+ }
413
+ /**
414
+ * Get current connection status
415
+ */
416
+ getStatus() {
417
+ return {
418
+ state: this.state,
419
+ rendererCount: this.renderers.size,
420
+ reactVersion: this.backendVersion,
421
+ error: this.error
422
+ };
423
+ }
424
+ /**
425
+ * Check if connected
426
+ */
427
+ isConnected() {
428
+ if (this.isExternallyAttached) {
429
+ return this.state === "connected";
430
+ }
431
+ return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
432
+ }
433
+ setState(state) {
434
+ this.state = state;
435
+ this.emit("stateChange", state);
436
+ }
437
+ setError(message) {
438
+ this.error = message;
439
+ this.setState("error");
440
+ }
441
+ reset() {
442
+ this.elements.clear();
443
+ this.rootIDs.clear();
444
+ this.renderers.clear();
445
+ this.elementToRenderer.clear();
446
+ this.elementErrors.clear();
447
+ this.elementWarnings.clear();
448
+ this.isProfiling = false;
449
+ this.profilingData = null;
450
+ this.isInspectingNative = false;
451
+ this.capabilities = { ...DEFAULT_CAPABILITIES };
452
+ this.capabilitiesNegotiated = false;
453
+ this.stopStaleRequestCleanup();
454
+ }
455
+ // ═══════════════════════════════════════════════════════════════════════════
456
+ // REQUEST MANAGEMENT (Phase 1.5 & 1.6)
457
+ // ═══════════════════════════════════════════════════════════════════════════
458
+ /**
459
+ * Generate unique request ID (Phase 1.6)
460
+ */
461
+ nextRequestId() {
462
+ return ++this.requestIdCounter;
463
+ }
464
+ /**
465
+ * Create a pending request with proper cleanup (Phase 1.5)
466
+ */
467
+ createPending(key, operation, timeout) {
468
+ return new Promise((resolve, reject) => {
469
+ const cleanup = () => {
470
+ const req = this.pendingRequests.get(key);
471
+ if (req) {
472
+ clearTimeout(req.timeout);
473
+ this.pendingRequests.delete(key);
474
+ }
475
+ };
476
+ const timeoutMs = timeout ?? this.config.timeout;
477
+ const timeoutId = setTimeout(() => {
478
+ this.logger.warn("Request timeout", { key, operation, timeout: timeoutMs });
479
+ cleanup();
480
+ reject(new TimeoutError(operation, timeoutMs, { key }));
481
+ }, timeoutMs);
482
+ this.pendingRequests.set(key, {
483
+ resolve: (value) => {
484
+ cleanup();
485
+ resolve(value);
486
+ },
487
+ reject: (error) => {
488
+ cleanup();
489
+ reject(error);
490
+ },
491
+ timeout: timeoutId,
492
+ createdAt: Date.now(),
493
+ operation
494
+ });
495
+ });
496
+ }
497
+ /**
498
+ * Resolve a pending request
499
+ */
500
+ resolvePending(key, value) {
501
+ const pending = this.pendingRequests.get(key);
502
+ if (pending) {
503
+ this.logger.debug("Resolving request", { key, operation: pending.operation });
504
+ pending.resolve(value);
505
+ }
506
+ }
507
+ /**
508
+ * Resolve a correlated request using responseID/requestID/fallback pattern.
509
+ * Handles the common pattern of: responseID -> requestID -> element ID fallback.
510
+ *
511
+ * @param prefix - Key prefix (e.g., 'inspect', 'owners', 'nativeStyle')
512
+ * @param payload - Response payload with optional responseID, requestID, and id
513
+ * @param result - Value to resolve the promise with
514
+ */
515
+ resolveCorrelatedRequest(prefix, payload, result) {
516
+ let key;
517
+ if (payload.responseID !== void 0) {
518
+ key = `${prefix}_${payload.responseID}`;
519
+ } else if (payload.requestID !== void 0) {
520
+ key = `${prefix}_${payload.requestID}`;
521
+ } else {
522
+ key = `${prefix}_${payload.id ?? "unknown"}`;
523
+ }
524
+ if (!this.pendingRequests.has(key) && payload.id !== void 0) {
525
+ const fallbackKey = `${prefix}_${payload.id}`;
526
+ const primaryKey = this.responseFallbackKeys.get(fallbackKey);
527
+ if (primaryKey && this.pendingRequests.has(primaryKey)) {
528
+ key = primaryKey;
529
+ }
530
+ this.responseFallbackKeys.delete(fallbackKey);
531
+ } else if (payload.id !== void 0) {
532
+ this.responseFallbackKeys.delete(`${prefix}_${payload.id}`);
533
+ }
534
+ this.resolvePending(key, result);
535
+ }
536
+ /**
537
+ * Store a fallback key mapping for request correlation.
538
+ * Call this when sending a request that uses requestID.
539
+ *
540
+ * @param prefix - Key prefix (e.g., 'inspect', 'owners')
541
+ * @param requestID - The requestID being sent
542
+ * @param elementID - The element ID (used as fallback key)
543
+ */
544
+ storeFallbackKey(prefix, requestID, elementID) {
545
+ const fallbackKey = `${prefix}_${elementID}`;
546
+ const primaryKey = `${prefix}_${requestID}`;
547
+ this.responseFallbackKeys.set(fallbackKey, primaryKey);
548
+ }
549
+ /**
550
+ * Start periodic cleanup of stale requests (Phase 1.5)
551
+ */
552
+ startStaleRequestCleanup() {
553
+ this.staleRequestCleanupTimer = setInterval(() => {
554
+ const now = Date.now();
555
+ const maxAge = this.config.timeout * 2;
556
+ for (const [key, req] of this.pendingRequests) {
557
+ const age = now - req.createdAt;
558
+ if (age > maxAge) {
559
+ this.logger.warn("Cleaning stale request", { key, operation: req.operation, age });
560
+ clearTimeout(req.timeout);
561
+ this.pendingRequests.delete(key);
562
+ req.reject(new TimeoutError(req.operation, age, { key, stale: true }));
563
+ }
564
+ }
565
+ }, 6e4);
566
+ }
567
+ /**
568
+ * Stop stale request cleanup
569
+ */
570
+ stopStaleRequestCleanup() {
571
+ if (this.staleRequestCleanupTimer) {
572
+ clearInterval(this.staleRequestCleanupTimer);
573
+ this.staleRequestCleanupTimer = null;
574
+ }
575
+ }
576
+ // ═══════════════════════════════════════════════════════════════════════════
577
+ // MESSAGE HANDLING
578
+ // ═══════════════════════════════════════════════════════════════════════════
579
+ send(event, payload) {
580
+ if (this.isExternallyAttached && this.externalSendFn) {
581
+ this.logger.debug("Sending message via external", { event });
582
+ this.externalSendFn(event, payload);
583
+ return;
584
+ }
585
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
586
+ throw new ConnectionError("Not connected");
587
+ }
588
+ const message = JSON.stringify({ event, payload });
589
+ this.logger.debug("Sending message", { event, payloadSize: message.length });
590
+ this.ws.send(message);
591
+ }
592
+ handleMessage(data) {
593
+ this.lastMessageAt = Date.now();
594
+ let parsed;
595
+ try {
596
+ parsed = JSON.parse(data);
597
+ } catch (err) {
598
+ const error = err instanceof Error ? err.message : "Unknown parse error";
599
+ this.logger.error("Failed to parse message", { error, dataPreview: data.substring(0, 100) });
600
+ this.emit("parseError", { data: data.substring(0, 100), error });
601
+ return;
602
+ }
603
+ const { event, payload } = parsed;
604
+ if (!event) {
605
+ this.logger.warn("Message missing event field", { dataPreview: data.substring(0, 100) });
606
+ return;
607
+ }
608
+ this.logger.debug("Received message", { event });
609
+ switch (event) {
610
+ case "operations":
611
+ this.handleOperations(payload);
612
+ break;
613
+ case "inspectedElement":
614
+ this.handleInspectedElement(payload);
615
+ break;
616
+ case "ownersList":
617
+ this.handleOwnersList(payload);
618
+ break;
619
+ case "profilingData":
620
+ this.handleProfilingData(payload);
621
+ break;
622
+ case "profilingStatus":
623
+ this.handleProfilingStatus(payload);
624
+ break;
625
+ case "backendVersion":
626
+ this.backendVersion = payload;
627
+ this.logger.info("Backend version", { version: this.backendVersion });
628
+ break;
629
+ case "bridge":
630
+ case "bridgeProtocol":
631
+ this.logger.debug("Bridge protocol", { payload });
632
+ break;
633
+ case "renderer":
634
+ this.handleRenderer(payload);
635
+ break;
636
+ case "unsupportedRendererVersion":
637
+ this.logger.error("Unsupported React version", { version: payload });
638
+ this.setError(`Unsupported React version: ${payload}`);
639
+ break;
640
+ case "shutdown":
641
+ this.logger.info("Backend shutdown received");
642
+ this.disconnect();
643
+ break;
644
+ case "NativeStyleEditor_styleAndLayout":
645
+ this.handleNativeStyleResponse(payload);
646
+ break;
647
+ // ═══════════════════════════════════════════════════════════════════════
648
+ // Phase 2.1: Additional Message Handlers
649
+ // ═══════════════════════════════════════════════════════════════════════
650
+ case "isBackendStorageAPISupported":
651
+ this.handleStorageSupport(payload);
652
+ break;
653
+ case "isSynchronousXHRSupported":
654
+ this.handleXHRSupport(payload);
655
+ break;
656
+ case "getSupportedRendererInterfaces":
657
+ this.handleRendererInterfaces(payload);
658
+ break;
659
+ case "updateComponentFilters":
660
+ this.logger.debug("Component filters updated");
661
+ this.emit("filtersUpdated");
662
+ break;
663
+ case "savedToClipboard":
664
+ this.logger.debug("Content saved to clipboard");
665
+ this.handleClipboardResponse(payload);
666
+ break;
667
+ case "viewAttributeSourceResult":
668
+ this.handleAttributeSourceResult(payload);
669
+ break;
670
+ case "overrideContextResult":
671
+ this.handleOverrideContextResponse(payload);
672
+ break;
673
+ case "inspectingNativeStarted":
674
+ this.isInspectingNative = true;
675
+ this.logger.info("Native inspection started");
676
+ this.emit("inspectingNativeStarted");
677
+ break;
678
+ case "inspectingNativeStopped":
679
+ this.isInspectingNative = false;
680
+ this.handleInspectingNativeStopped(payload);
681
+ break;
682
+ case "captureScreenshotResult":
683
+ this.handleScreenshotResponse(payload);
684
+ break;
685
+ default:
686
+ this.logger.debug("Unknown message type", { event });
687
+ this.emit("unknown", { event, payload });
688
+ }
689
+ }
690
+ handleRenderer(payload) {
691
+ const renderer = {
692
+ id: payload.id,
693
+ version: payload.rendererVersion,
694
+ packageName: payload.rendererPackageName,
695
+ rootIDs: /* @__PURE__ */ new Set(),
696
+ elementIDs: /* @__PURE__ */ new Set()
697
+ };
698
+ this.renderers.set(payload.id, renderer);
699
+ this.logger.info("Renderer connected", { id: payload.id, version: payload.rendererVersion });
700
+ this.emit("renderer", { id: payload.id, rendererVersion: payload.rendererVersion });
701
+ }
702
+ // ═══════════════════════════════════════════════════════════════════════════
703
+ // Phase 2.1: Capability Detection Handlers
704
+ // ═══════════════════════════════════════════════════════════════════════════
705
+ handleStorageSupport(payload) {
706
+ this.capabilities.isBackendStorageAPISupported = payload.isSupported;
707
+ this.logger.debug("Storage API support", { isSupported: payload.isSupported });
708
+ this.checkCapabilitiesComplete();
709
+ }
710
+ handleXHRSupport(payload) {
711
+ this.capabilities.isSynchronousXHRSupported = payload.isSupported;
712
+ this.logger.debug("Synchronous XHR support", { isSupported: payload.isSupported });
713
+ this.checkCapabilitiesComplete();
714
+ }
715
+ handleRendererInterfaces(payload) {
716
+ this.logger.debug("Renderer interfaces received", { count: payload.rendererInterfaces?.length ?? 0 });
717
+ if (payload.rendererInterfaces) {
718
+ for (const iface of payload.rendererInterfaces) {
719
+ const renderer = this.renderers.get(iface.id);
720
+ if (renderer) {
721
+ renderer.version = iface.version;
722
+ renderer.packageName = iface.renderer;
723
+ }
724
+ const versionNum = parseFloat(iface.version);
725
+ if (versionNum >= 18) {
726
+ this.capabilities.supportsProfilingChangeDescriptions = true;
727
+ this.capabilities.supportsTimeline = true;
728
+ this.capabilities.supportsErrorBoundaryTesting = true;
729
+ }
730
+ }
731
+ }
732
+ this.checkCapabilitiesComplete();
733
+ }
734
+ checkCapabilitiesComplete() {
735
+ if (!this.capabilitiesNegotiated) {
736
+ this.capabilitiesNegotiated = true;
737
+ this.logger.info("Protocol capabilities negotiated", { capabilities: this.capabilities });
738
+ this.emit("capabilitiesNegotiated", this.capabilities);
739
+ }
740
+ }
741
+ handleAttributeSourceResult(payload) {
742
+ this.resolveCorrelatedRequest("attributeSource", payload, payload.source);
743
+ if (payload.source) {
744
+ this.emit("attributeSource", payload.source);
745
+ }
746
+ }
747
+ handleInspectingNativeStopped(payload) {
748
+ this.logger.info("Native inspection stopped", { elementID: payload.elementID });
749
+ this.resolvePending("inspectNative", payload.elementID);
750
+ this.emit("inspectingNativeStopped", payload.elementID);
751
+ }
752
+ handleNativeStyleResponse(payload) {
753
+ this.resolveCorrelatedRequest("nativeStyle", payload, { style: payload.style, layout: payload.layout });
754
+ }
755
+ handleClipboardResponse(payload) {
756
+ if (payload.responseID !== void 0) {
757
+ this.resolvePending(`clipboard_${payload.responseID}`, { success: true });
758
+ } else {
759
+ for (const pendingKey of this.pendingRequests.keys()) {
760
+ if (pendingKey.startsWith("clipboard_")) {
761
+ this.resolvePending(pendingKey, { success: true });
762
+ break;
763
+ }
764
+ }
765
+ }
766
+ }
767
+ handleOverrideContextResponse(payload) {
768
+ this.resolveCorrelatedRequest("overrideContext", payload, payload);
769
+ }
770
+ handleScreenshotResponse(payload) {
771
+ this.resolveCorrelatedRequest("screenshot", payload, payload);
772
+ }
773
+ // ═══════════════════════════════════════════════════════════════════════════
774
+ // OPERATIONS PARSING (Phase 1.4: Bounds Checking)
775
+ // ═══════════════════════════════════════════════════════════════════════════
776
+ /**
777
+ * Decode UTF-8 string from operations array
778
+ * Based on react-devtools-shared/src/utils.js utfDecodeStringWithRanges
779
+ */
780
+ utfDecodeString(operations, start, end) {
781
+ let result = "";
782
+ for (let i = start; i <= end; i++) {
783
+ const charCode = operations[i];
784
+ if (typeof charCode === "number" && charCode >= 0 && charCode <= 1114111) {
785
+ result += String.fromCodePoint(charCode);
786
+ }
787
+ }
788
+ return result;
789
+ }
790
+ handleOperations(operations) {
791
+ if (!Array.isArray(operations)) {
792
+ this.logger.warn("Invalid operations: not an array");
793
+ return;
794
+ }
795
+ if (operations.length < 3) {
796
+ this.logger.debug("Empty operations array");
797
+ return;
798
+ }
799
+ const rendererID = operations[0];
800
+ const rootID = operations[1];
801
+ if (rootID !== 0) {
802
+ this.rootIDs.add(rootID);
803
+ }
804
+ let i = 2;
805
+ const stringTableSize = operations[i];
806
+ i++;
807
+ const stringTable = [null];
808
+ const stringTableEnd = i + stringTableSize;
809
+ while (i < stringTableEnd && i < operations.length) {
810
+ const strLength = operations[i];
811
+ i++;
812
+ if (strLength > 0 && i + strLength - 1 < operations.length) {
813
+ const str = this.utfDecodeString(operations, i, i + strLength - 1);
814
+ stringTable.push(str);
815
+ i += strLength;
816
+ } else {
817
+ stringTable.push("");
818
+ }
819
+ }
820
+ this.logger.debug("Parsed string table", {
821
+ rendererID,
822
+ rootID,
823
+ stringCount: stringTable.length - 1,
824
+ strings: stringTable.slice(1),
825
+ operationsStart: i
826
+ });
827
+ while (i < operations.length) {
828
+ const op = operations[i];
829
+ if (typeof op !== "number") {
830
+ this.logger.warn("Invalid operation code", { index: i, value: op });
831
+ break;
832
+ }
833
+ switch (op) {
834
+ case TREE_OP.ADD:
835
+ i = this.processAddOperation(operations, i + 1, rendererID, stringTable);
836
+ break;
837
+ case TREE_OP.REMOVE:
838
+ i = this.processRemoveOperation(operations, i + 1);
839
+ break;
840
+ case TREE_OP.REORDER:
841
+ i = this.processReorderOperation(operations, i + 1);
842
+ break;
843
+ case TREE_OP.UPDATE_TREE_BASE_DURATION:
844
+ i += 3;
845
+ break;
846
+ case TREE_OP.UPDATE_ERRORS_OR_WARNINGS:
847
+ i = this.processErrorsWarningsOperation(operations, i + 1);
848
+ break;
849
+ default:
850
+ this.logger.warn("Unknown operation code", { code: op, index: i });
851
+ i++;
852
+ }
853
+ if (i <= 0) {
854
+ this.logger.error("Operations parser stuck", { index: i });
855
+ break;
856
+ }
857
+ }
858
+ this.emit("operationsComplete");
859
+ }
860
+ /**
861
+ * Process ADD operation with string table lookup
862
+ * Based on react-devtools-shared/src/devtools/store.js onBridgeOperations
863
+ *
864
+ * Root format: [id, type=11, isStrictModeCompliant, profilerFlags, supportsStrictMode, hasOwnerMetadata]
865
+ * Non-root format: [id, type, parentID, ownerID, displayNameStringID, keyStringID, namePropStringID]
866
+ */
867
+ processAddOperation(ops, i, rendererID, stringTable) {
868
+ if (i + 2 > ops.length) {
869
+ this.logger.warn("ADD operation: insufficient data for id/type", { index: i, available: ops.length - i });
870
+ return ops.length;
871
+ }
872
+ const id = ops[i++];
873
+ const type = ops[i++];
874
+ if (type === 11) {
875
+ if (i + 4 > ops.length) {
876
+ this.logger.warn("ADD root: insufficient data", { index: i, available: ops.length - i, needed: 4 });
877
+ return ops.length;
878
+ }
879
+ const isStrictModeCompliant = ops[i++] > 0;
880
+ const profilerFlags = ops[i++];
881
+ const supportsStrictMode = ops[i++] > 0;
882
+ const hasOwnerMetadata = ops[i++] > 0;
883
+ const element2 = {
884
+ id,
885
+ parentID: null,
886
+ displayName: "Root",
887
+ type: "root",
888
+ key: null,
889
+ depth: 0,
890
+ weight: 1,
891
+ ownerID: null,
892
+ hasChildren: false,
893
+ env: null,
894
+ hocDisplayNames: null
895
+ };
896
+ this.rootIDs.add(id);
897
+ this.elements.set(id, element2);
898
+ this.elementToRenderer.set(id, rendererID);
899
+ const renderer2 = this.renderers.get(rendererID);
900
+ if (renderer2) {
901
+ renderer2.rootIDs.add(id);
902
+ renderer2.elementIDs.add(id);
903
+ }
904
+ this.logger.debug("Added root element", {
905
+ id,
906
+ rendererID,
907
+ isStrictModeCompliant,
908
+ profilerFlags,
909
+ supportsStrictMode,
910
+ hasOwnerMetadata
911
+ });
912
+ this.emit("elementAdded", element2);
913
+ return i;
914
+ }
915
+ if (i + 5 > ops.length) {
916
+ this.logger.warn("ADD operation: insufficient data", { index: i, available: ops.length - i, needed: 5 });
917
+ return ops.length;
918
+ }
919
+ const parentID = ops[i++];
920
+ const ownerID = ops[i++];
921
+ const displayNameStringID = ops[i++];
922
+ const keyStringID = ops[i++];
923
+ i++;
924
+ const displayName = displayNameStringID > 0 && displayNameStringID < stringTable.length ? stringTable[displayNameStringID] ?? "Unknown" : "Unknown";
925
+ const key = keyStringID > 0 && keyStringID < stringTable.length ? stringTable[keyStringID] : null;
926
+ const element = {
927
+ id,
928
+ parentID: parentID === 0 ? null : parentID,
929
+ displayName,
930
+ type: ELEMENT_TYPE_MAP[type] ?? "function",
931
+ key,
932
+ depth: 0,
933
+ weight: 1,
934
+ ownerID: ownerID === 0 ? null : ownerID,
935
+ hasChildren: false,
936
+ env: null,
937
+ hocDisplayNames: null
938
+ };
939
+ if (element.parentID !== null) {
940
+ const parent = this.elements.get(element.parentID);
941
+ if (parent) {
942
+ element.depth = parent.depth + 1;
943
+ parent.hasChildren = true;
944
+ }
945
+ }
946
+ this.elements.set(id, element);
947
+ this.elementToRenderer.set(id, rendererID);
948
+ const renderer = this.renderers.get(rendererID);
949
+ if (renderer) {
950
+ renderer.elementIDs.add(id);
951
+ }
952
+ this.logger.debug("Added element", { id, displayName, type: element.type, parentID });
953
+ this.emit("elementAdded", element);
954
+ return i;
955
+ }
956
+ /**
957
+ * Process REMOVE operation with bounds checking
958
+ */
959
+ processRemoveOperation(ops, i) {
960
+ if (i >= ops.length) {
961
+ this.logger.warn("REMOVE operation: missing count");
962
+ return ops.length;
963
+ }
964
+ const count = ops[i++];
965
+ if (count < 0 || count > 1e5) {
966
+ this.logger.warn("REMOVE operation: invalid count", { count });
967
+ return ops.length;
968
+ }
969
+ if (i + count > ops.length) {
970
+ this.logger.warn("REMOVE operation: not enough IDs", { count, available: ops.length - i });
971
+ return ops.length;
972
+ }
973
+ for (let j = 0; j < count; j++) {
974
+ const id = ops[i++];
975
+ const element = this.elements.get(id);
976
+ if (element) {
977
+ const rendererID = this.elementToRenderer.get(id);
978
+ if (rendererID !== void 0) {
979
+ const renderer = this.renderers.get(rendererID);
980
+ if (renderer) {
981
+ renderer.elementIDs.delete(id);
982
+ renderer.rootIDs.delete(id);
983
+ }
984
+ this.elementToRenderer.delete(id);
985
+ }
986
+ this.elements.delete(id);
987
+ this.rootIDs.delete(id);
988
+ this.elementErrors.delete(id);
989
+ this.elementWarnings.delete(id);
990
+ this.emit("elementRemoved", element);
991
+ }
992
+ }
993
+ return i;
994
+ }
995
+ /**
996
+ * Process REORDER operation with bounds checking
997
+ */
998
+ processReorderOperation(ops, i) {
999
+ if (i + 1 >= ops.length) {
1000
+ this.logger.warn("REORDER operation: insufficient data");
1001
+ return ops.length;
1002
+ }
1003
+ const id = ops[i++];
1004
+ const childCount = ops[i++];
1005
+ if (childCount < 0 || childCount > 1e5) {
1006
+ this.logger.warn("REORDER operation: invalid childCount", { childCount });
1007
+ return ops.length;
1008
+ }
1009
+ if (i + childCount > ops.length) {
1010
+ this.logger.warn("REORDER operation: not enough child IDs", { childCount, available: ops.length - i });
1011
+ return ops.length;
1012
+ }
1013
+ i += childCount;
1014
+ this.emit("elementReordered", { id, childCount });
1015
+ return i;
1016
+ }
1017
+ /**
1018
+ * Process ERRORS/WARNINGS operation with bounds checking
1019
+ */
1020
+ processErrorsWarningsOperation(ops, i) {
1021
+ if (i + 2 >= ops.length) {
1022
+ this.logger.warn("ERRORS_WARNINGS operation: insufficient data");
1023
+ return ops.length;
1024
+ }
1025
+ const id = ops[i++];
1026
+ const errorCount = ops[i++];
1027
+ const warningCount = ops[i++];
1028
+ if (errorCount > 0) {
1029
+ this.elementErrors.set(id, []);
1030
+ } else {
1031
+ this.elementErrors.delete(id);
1032
+ }
1033
+ if (warningCount > 0) {
1034
+ this.elementWarnings.set(id, []);
1035
+ } else {
1036
+ this.elementWarnings.delete(id);
1037
+ }
1038
+ return i;
1039
+ }
1040
+ // ═══════════════════════════════════════════════════════════════════════════
1041
+ // RESPONSE HANDLERS (Phase 1.6: ID Correlation)
1042
+ // ═══════════════════════════════════════════════════════════════════════════
1043
+ handleInspectedElement(payload) {
1044
+ this.resolveCorrelatedRequest("inspect", payload, payload);
1045
+ }
1046
+ handleOwnersList(payload) {
1047
+ this.resolveCorrelatedRequest("owners", payload, payload.owners);
1048
+ }
1049
+ handleProfilingData(payload) {
1050
+ this.profilingData = payload;
1051
+ this.resolvePending("profilingData", payload);
1052
+ }
1053
+ handleProfilingStatus(payload) {
1054
+ this.isProfiling = payload.isProfiling;
1055
+ this.resolvePending("profilingStatus", payload);
1056
+ }
1057
+ // ═══════════════════════════════════════════════════════════════════════════
1058
+ // PUBLIC API
1059
+ // ═══════════════════════════════════════════════════════════════════════════
1060
+ getComponentTree(rootID, maxDepth) {
1061
+ const result = [];
1062
+ const rootsToProcess = rootID ? [rootID] : Array.from(this.rootIDs);
1063
+ for (const rid of rootsToProcess) {
1064
+ const root = this.elements.get(rid);
1065
+ if (!root) continue;
1066
+ const elements = [];
1067
+ const collectElements = (id, depth) => {
1068
+ const el = this.elements.get(id);
1069
+ if (!el) return;
1070
+ if (maxDepth !== void 0 && depth > maxDepth) return;
1071
+ elements.push(el);
1072
+ for (const [, child] of this.elements) {
1073
+ if (child.parentID === id) {
1074
+ collectElements(child.id, depth + 1);
1075
+ }
1076
+ }
1077
+ };
1078
+ collectElements(rid, 0);
1079
+ result.push({
1080
+ rootID: rid,
1081
+ displayName: root.displayName,
1082
+ elements
1083
+ });
1084
+ }
1085
+ return result;
1086
+ }
1087
+ getElementById(id) {
1088
+ return this.elements.get(id) ?? null;
1089
+ }
1090
+ searchComponents(query, caseSensitive = false, isRegex = false) {
1091
+ const matches = [];
1092
+ let pattern = null;
1093
+ if (isRegex) {
1094
+ try {
1095
+ pattern = new RegExp(query, caseSensitive ? "" : "i");
1096
+ } catch {
1097
+ this.logger.warn("Invalid regex pattern", { query });
1098
+ return [];
1099
+ }
1100
+ }
1101
+ const searchLower = caseSensitive ? query : query.toLowerCase();
1102
+ for (const [, element] of this.elements) {
1103
+ const name = caseSensitive ? element.displayName : element.displayName.toLowerCase();
1104
+ if (pattern) {
1105
+ if (pattern.test(element.displayName)) {
1106
+ matches.push(element);
1107
+ }
1108
+ } else if (name.includes(searchLower)) {
1109
+ matches.push(element);
1110
+ }
1111
+ }
1112
+ return matches;
1113
+ }
1114
+ /**
1115
+ * Inspect element with request ID correlation (Phase 1.6)
1116
+ */
1117
+ async inspectElement(id, paths) {
1118
+ this.ensureConnected();
1119
+ const rendererID = this.getRendererIDForElement(id);
1120
+ if (rendererID === null) {
1121
+ return { type: "not-found", id };
1122
+ }
1123
+ const requestID = this.nextRequestId();
1124
+ const primaryKey = `inspect_${requestID}`;
1125
+ const promise = this.createPending(primaryKey, `inspectElement(${id})`);
1126
+ this.storeFallbackKey("inspect", requestID, id);
1127
+ this.send("inspectElement", {
1128
+ id,
1129
+ rendererID,
1130
+ requestID,
1131
+ forceFullData: true,
1132
+ path: paths?.[0] ?? null
1133
+ });
1134
+ return promise;
1135
+ }
1136
+ /**
1137
+ * Get owners list with request ID correlation (Phase 1.6)
1138
+ */
1139
+ async getOwnersList(id) {
1140
+ this.ensureConnected();
1141
+ const rendererID = this.getRendererIDForElement(id);
1142
+ if (rendererID === null) {
1143
+ return [];
1144
+ }
1145
+ const requestID = this.nextRequestId();
1146
+ const primaryKey = `owners_${requestID}`;
1147
+ const promise = this.createPending(primaryKey, `getOwnersList(${id})`);
1148
+ this.storeFallbackKey("owners", requestID, id);
1149
+ this.send("getOwnersList", { id, rendererID, requestID });
1150
+ return promise;
1151
+ }
1152
+ highlightElement(id) {
1153
+ this.ensureConnected();
1154
+ const rendererID = this.getRendererIDForElement(id);
1155
+ if (rendererID === null) return;
1156
+ this.send("highlightNativeElement", { id, rendererID });
1157
+ }
1158
+ clearHighlight() {
1159
+ if (this.isConnected()) {
1160
+ this.send("clearNativeElementHighlight", {});
1161
+ }
1162
+ }
1163
+ scrollToElement(id) {
1164
+ this.ensureConnected();
1165
+ const rendererID = this.getRendererIDForElement(id);
1166
+ if (rendererID === null) return;
1167
+ this.send("scrollToNativeElement", { id, rendererID });
1168
+ }
1169
+ logToConsole(id) {
1170
+ this.ensureConnected();
1171
+ const rendererID = this.getRendererIDForElement(id);
1172
+ if (rendererID === null) return;
1173
+ this.send("logElementToConsole", { id, rendererID });
1174
+ }
1175
+ storeAsGlobal(id, path, count) {
1176
+ this.ensureConnected();
1177
+ const rendererID = this.getRendererIDForElement(id);
1178
+ if (rendererID === null) return;
1179
+ this.send("storeAsGlobal", { id, rendererID, path, count });
1180
+ }
1181
+ viewElementSource(id) {
1182
+ this.ensureConnected();
1183
+ const rendererID = this.getRendererIDForElement(id);
1184
+ if (rendererID === null) return;
1185
+ this.send("viewElementSource", { id, rendererID });
1186
+ }
1187
+ // ═══════════════════════════════════════════════════════════════════════════
1188
+ // OVERRIDES
1189
+ // ═══════════════════════════════════════════════════════════════════════════
1190
+ overrideValueAtPath(target, id, path, value, hookIndex) {
1191
+ this.ensureConnected();
1192
+ const rendererID = this.getRendererIDForElement(id);
1193
+ if (rendererID === null) return;
1194
+ this.send("overrideValueAtPath", {
1195
+ type: target,
1196
+ id,
1197
+ rendererID,
1198
+ path,
1199
+ value,
1200
+ hookID: hookIndex
1201
+ });
1202
+ }
1203
+ deletePath(target, id, path, hookIndex) {
1204
+ this.ensureConnected();
1205
+ const rendererID = this.getRendererIDForElement(id);
1206
+ if (rendererID === null) return;
1207
+ this.send("deletePath", {
1208
+ type: target,
1209
+ id,
1210
+ rendererID,
1211
+ path,
1212
+ hookID: hookIndex
1213
+ });
1214
+ }
1215
+ renamePath(target, id, path, oldKey, newKey, hookIndex) {
1216
+ this.ensureConnected();
1217
+ const rendererID = this.getRendererIDForElement(id);
1218
+ if (rendererID === null) return;
1219
+ this.send("renamePath", {
1220
+ type: target,
1221
+ id,
1222
+ rendererID,
1223
+ path,
1224
+ oldKey,
1225
+ newKey,
1226
+ hookID: hookIndex
1227
+ });
1228
+ }
1229
+ // ═══════════════════════════════════════════════════════════════════════════
1230
+ // ERROR / SUSPENSE
1231
+ // ═══════════════════════════════════════════════════════════════════════════
1232
+ overrideError(id, isErrored) {
1233
+ this.ensureConnected();
1234
+ const rendererID = this.getRendererIDForElement(id);
1235
+ if (rendererID === null) return;
1236
+ this.send("overrideError", { id, rendererID, forceError: isErrored });
1237
+ }
1238
+ overrideSuspense(id, isSuspended) {
1239
+ this.ensureConnected();
1240
+ const rendererID = this.getRendererIDForElement(id);
1241
+ if (rendererID === null) return;
1242
+ this.send("overrideSuspense", { id, rendererID, forceFallback: isSuspended });
1243
+ }
1244
+ clearErrorsAndWarnings(id) {
1245
+ this.ensureConnected();
1246
+ if (id !== void 0) {
1247
+ const rendererID = this.getRendererIDForElement(id);
1248
+ if (rendererID === null) return;
1249
+ this.send("clearErrorsForFiberID", { id, rendererID });
1250
+ } else {
1251
+ this.send("clearErrorsAndWarnings", {});
1252
+ }
1253
+ }
1254
+ getErrorsAndWarnings() {
1255
+ return {
1256
+ errors: new Map(this.elementErrors),
1257
+ warnings: new Map(this.elementWarnings)
1258
+ };
1259
+ }
1260
+ // ═══════════════════════════════════════════════════════════════════════════
1261
+ // PROFILING
1262
+ // ═══════════════════════════════════════════════════════════════════════════
1263
+ startProfiling(recordTimeline = false, recordChangeDescriptions = true) {
1264
+ this.ensureConnected();
1265
+ this.send("startProfiling", { recordTimeline, recordChangeDescriptions });
1266
+ this.isProfiling = true;
1267
+ this.logger.info("Profiling started", { recordTimeline, recordChangeDescriptions });
1268
+ }
1269
+ stopProfiling() {
1270
+ this.ensureConnected();
1271
+ this.send("stopProfiling", {});
1272
+ this.isProfiling = false;
1273
+ this.logger.info("Profiling stopped");
1274
+ }
1275
+ async getProfilingData() {
1276
+ if (!this.isProfiling && this.profilingData) {
1277
+ return this.profilingData;
1278
+ }
1279
+ this.ensureConnected();
1280
+ const promise = this.createPending("profilingData", "getProfilingData");
1281
+ this.send("getProfilingData", {});
1282
+ return promise;
1283
+ }
1284
+ getProfilingStatus() {
1285
+ return { isProfiling: this.isProfiling };
1286
+ }
1287
+ // ═══════════════════════════════════════════════════════════════════════════
1288
+ // FILTERS
1289
+ // ═══════════════════════════════════════════════════════════════════════════
1290
+ setComponentFilters(filters) {
1291
+ this.ensureConnected();
1292
+ this.send("updateComponentFilters", { componentFilters: filters });
1293
+ }
1294
+ setTraceUpdatesEnabled(enabled) {
1295
+ this.ensureConnected();
1296
+ this.send("setTraceUpdatesEnabled", { enabled });
1297
+ }
1298
+ // ═══════════════════════════════════════════════════════════════════════════
1299
+ // REACT NATIVE SPECIFIC
1300
+ // ═══════════════════════════════════════════════════════════════════════════
1301
+ async getNativeStyle(id) {
1302
+ this.ensureConnected();
1303
+ const rendererID = this.getRendererIDForElement(id);
1304
+ if (rendererID === null) {
1305
+ return { style: null, layout: null };
1306
+ }
1307
+ const requestID = this.nextRequestId();
1308
+ const primaryKey = `nativeStyle_${requestID}`;
1309
+ const promise = this.createPending(primaryKey, `getNativeStyle(${id})`);
1310
+ this.storeFallbackKey("nativeStyle", requestID, id);
1311
+ this.send("NativeStyleEditor_measure", { id, rendererID, requestID });
1312
+ return promise;
1313
+ }
1314
+ setNativeStyle(id, property, value) {
1315
+ this.ensureConnected();
1316
+ const rendererID = this.getRendererIDForElement(id);
1317
+ if (rendererID === null) return;
1318
+ this.send("NativeStyleEditor_setValue", { id, rendererID, name: property, value });
1319
+ }
1320
+ // ═══════════════════════════════════════════════════════════════════════════
1321
+ // PHASE 2.1: ADDITIONAL PUBLIC API
1322
+ // ═══════════════════════════════════════════════════════════════════════════
1323
+ /**
1324
+ * Save content to clipboard
1325
+ */
1326
+ async saveToClipboard(value) {
1327
+ this.ensureConnected();
1328
+ const requestID = this.nextRequestId();
1329
+ const primaryKey = `clipboard_${requestID}`;
1330
+ const promise = this.createPending(primaryKey, "saveToClipboard");
1331
+ this.send("saveToClipboard", { value, requestID });
1332
+ return Promise.race([
1333
+ promise,
1334
+ new Promise(
1335
+ (resolve) => setTimeout(() => resolve({ success: true }), 500)
1336
+ )
1337
+ ]);
1338
+ }
1339
+ /**
1340
+ * View attribute source location
1341
+ */
1342
+ async viewAttributeSource(id, path) {
1343
+ this.ensureConnected();
1344
+ const rendererID = this.getRendererIDForElement(id);
1345
+ if (rendererID === null) return null;
1346
+ const requestID = this.nextRequestId();
1347
+ const primaryKey = `attributeSource_${requestID}`;
1348
+ const promise = this.createPending(primaryKey, `viewAttributeSource(${id})`);
1349
+ this.storeFallbackKey("attributeSource", requestID, id);
1350
+ this.send("viewAttributeSource", { id, rendererID, path, requestID });
1351
+ return promise;
1352
+ }
1353
+ /**
1354
+ * Override context value
1355
+ */
1356
+ async overrideContext(id, path, value) {
1357
+ this.ensureConnected();
1358
+ const rendererID = this.getRendererIDForElement(id);
1359
+ if (rendererID === null) return false;
1360
+ const requestID = this.nextRequestId();
1361
+ const primaryKey = `overrideContext_${requestID}`;
1362
+ const promise = this.createPending(primaryKey, `overrideContext(${id})`);
1363
+ this.storeFallbackKey("overrideContext", requestID, id);
1364
+ this.send("overrideContext", { id, rendererID, path, value, requestID });
1365
+ try {
1366
+ const result = await promise;
1367
+ return result.success;
1368
+ } catch {
1369
+ return false;
1370
+ }
1371
+ }
1372
+ /**
1373
+ * Start native element inspection mode
1374
+ */
1375
+ startInspectingNative() {
1376
+ this.ensureConnected();
1377
+ this.send("startInspectingNative", {});
1378
+ }
1379
+ /**
1380
+ * Stop native element inspection mode
1381
+ * @param selectNextElement - Whether to select the next element under pointer
1382
+ * @returns The ID of the selected element, or null
1383
+ */
1384
+ async stopInspectingNative(selectNextElement = true) {
1385
+ this.ensureConnected();
1386
+ const promise = this.createPending("inspectNative", "stopInspectingNative");
1387
+ this.send("stopInspectingNative", { selectNextElement });
1388
+ return promise;
1389
+ }
1390
+ /**
1391
+ * Check if currently in native inspection mode
1392
+ */
1393
+ isInspectingNativeMode() {
1394
+ return this.isInspectingNative;
1395
+ }
1396
+ /**
1397
+ * Capture screenshot of an element
1398
+ */
1399
+ async captureScreenshot(id) {
1400
+ this.ensureConnected();
1401
+ const rendererID = this.getRendererIDForElement(id);
1402
+ if (rendererID === null) return null;
1403
+ const requestID = this.nextRequestId();
1404
+ const primaryKey = `screenshot_${requestID}`;
1405
+ const promise = this.createPending(primaryKey, `captureScreenshot(${id})`);
1406
+ this.storeFallbackKey("screenshot", requestID, id);
1407
+ this.send("captureScreenshot", { id, rendererID, requestID });
1408
+ try {
1409
+ const result = await promise;
1410
+ return result.screenshot;
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ }
1415
+ // ═══════════════════════════════════════════════════════════════════════════
1416
+ // PHASE 2.2: CAPABILITIES API
1417
+ // ═══════════════════════════════════════════════════════════════════════════
1418
+ /**
1419
+ * Get negotiated protocol capabilities
1420
+ */
1421
+ getCapabilities() {
1422
+ return { ...this.capabilities };
1423
+ }
1424
+ /**
1425
+ * Check if capabilities have been negotiated
1426
+ */
1427
+ hasNegotiatedCapabilities() {
1428
+ return this.capabilitiesNegotiated;
1429
+ }
1430
+ /**
1431
+ * Wait for capabilities negotiation to complete
1432
+ */
1433
+ async waitForCapabilities(timeout = 5e3) {
1434
+ if (this.capabilitiesNegotiated) {
1435
+ return this.getCapabilities();
1436
+ }
1437
+ return new Promise((resolve, reject) => {
1438
+ const timer = setTimeout(() => {
1439
+ this.removeListener("capabilitiesNegotiated", handler);
1440
+ reject(new TimeoutError("waitForCapabilities", timeout));
1441
+ }, timeout);
1442
+ const handler = (capabilities) => {
1443
+ clearTimeout(timer);
1444
+ resolve(capabilities);
1445
+ };
1446
+ this.once("capabilitiesNegotiated", handler);
1447
+ });
1448
+ }
1449
+ // ═══════════════════════════════════════════════════════════════════════════
1450
+ // PHASE 2.3: RENDERER MANAGEMENT API
1451
+ // ═══════════════════════════════════════════════════════════════════════════
1452
+ /**
1453
+ * Get all connected renderers
1454
+ */
1455
+ getRenderers() {
1456
+ return Array.from(this.renderers.values()).map((r) => ({
1457
+ ...r,
1458
+ rootIDs: new Set(r.rootIDs),
1459
+ elementIDs: new Set(r.elementIDs)
1460
+ }));
1461
+ }
1462
+ /**
1463
+ * Get renderer by ID
1464
+ */
1465
+ getRenderer(id) {
1466
+ const renderer = this.renderers.get(id);
1467
+ if (!renderer) return null;
1468
+ return {
1469
+ ...renderer,
1470
+ rootIDs: new Set(renderer.rootIDs),
1471
+ elementIDs: new Set(renderer.elementIDs)
1472
+ };
1473
+ }
1474
+ /**
1475
+ * Get renderer for a specific element
1476
+ */
1477
+ getRendererForElement(elementID) {
1478
+ const rendererID = this.getRendererIDForElement(elementID);
1479
+ if (rendererID === null) return null;
1480
+ return this.getRenderer(rendererID);
1481
+ }
1482
+ /**
1483
+ * Get elements for a specific renderer
1484
+ */
1485
+ getElementsByRenderer(rendererID) {
1486
+ const renderer = this.renderers.get(rendererID);
1487
+ if (!renderer) return [];
1488
+ return Array.from(renderer.elementIDs).map((id) => this.elements.get(id)).filter((el) => el !== void 0);
1489
+ }
1490
+ // ═══════════════════════════════════════════════════════════════════════════
1491
+ // HELPERS
1492
+ // ═══════════════════════════════════════════════════════════════════════════
1493
+ ensureConnected() {
1494
+ if (!this.isConnected()) {
1495
+ throw new ConnectionError("Not connected to DevTools");
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Get renderer ID for an element (Phase 2.3: Multi-renderer support)
1500
+ */
1501
+ getRendererIDForElement(id) {
1502
+ if (!this.elements.has(id)) {
1503
+ return null;
1504
+ }
1505
+ const rendererID = this.elementToRenderer.get(id);
1506
+ if (rendererID !== void 0) {
1507
+ return rendererID;
1508
+ }
1509
+ for (const renderer of this.renderers.values()) {
1510
+ if (renderer.elementIDs.has(id) || renderer.rootIDs.has(id)) {
1511
+ return renderer.id;
1512
+ }
1513
+ }
1514
+ if (this.renderers.size === 0) {
1515
+ return 1;
1516
+ }
1517
+ return this.renderers.keys().next().value ?? 1;
1518
+ }
1519
+ /**
1520
+ * Get last message timestamp (for health monitoring)
1521
+ */
1522
+ getLastMessageTime() {
1523
+ return this.lastMessageAt;
1524
+ }
1525
+ /**
1526
+ * Get pending request count (for monitoring)
1527
+ */
1528
+ getPendingRequestCount() {
1529
+ return this.pendingRequests.size;
1530
+ }
1531
+ };
1532
+
1533
+ // src/headless-server.ts
1534
+ import WebSocket2, { WebSocketServer } from "ws";
1535
+ import { createServer as createHttpServer } from "http";
1536
+ import { createServer as createHttpsServer } from "https";
1537
+ import { readFileSync } from "fs";
1538
+ import { createRequire } from "module";
1539
+ import { EventEmitter as EventEmitter2 } from "events";
1540
+ var require2 = createRequire(import.meta.url);
1541
+ var HeadlessDevToolsServer = class extends EventEmitter2 {
1542
+ _options;
1543
+ _httpServer = null;
1544
+ _wsServer = null;
1545
+ _socket = null;
1546
+ _state;
1547
+ _backendScript = null;
1548
+ constructor(options = {}) {
1549
+ super();
1550
+ this._options = {
1551
+ port: options.port ?? 8097,
1552
+ host: options.host ?? "localhost",
1553
+ httpsOptions: options.httpsOptions,
1554
+ logger: options.logger ?? noopLogger
1555
+ };
1556
+ this._state = {
1557
+ status: "stopped",
1558
+ port: this._options.port,
1559
+ host: this._options.host,
1560
+ connectedAt: null,
1561
+ error: null
1562
+ };
1563
+ this._loadBackendScript();
1564
+ }
1565
+ _loadBackendScript() {
1566
+ try {
1567
+ const backendPath = require2.resolve("react-devtools-core/dist/backend.js");
1568
+ this._backendScript = readFileSync(backendPath, "utf-8");
1569
+ this._options.logger.debug("Loaded backend.js from react-devtools-core");
1570
+ } catch (err) {
1571
+ this._options.logger.warn("Could not load backend.js - web apps will need to include it manually");
1572
+ }
1573
+ }
1574
+ get state() {
1575
+ return { ...this._state };
1576
+ }
1577
+ get isConnected() {
1578
+ return this._socket !== null && this._socket.readyState === WebSocket2.OPEN;
1579
+ }
1580
+ // External message listeners (for MCP bridge integration)
1581
+ _externalMessageListeners = [];
1582
+ /**
1583
+ * Add an external message listener that receives all messages from React app.
1584
+ * Used to relay messages to the MCP's DevToolsBridge.
1585
+ */
1586
+ addMessageListener(fn) {
1587
+ this._externalMessageListeners.push(fn);
1588
+ return () => {
1589
+ const idx = this._externalMessageListeners.indexOf(fn);
1590
+ if (idx >= 0) this._externalMessageListeners.splice(idx, 1);
1591
+ };
1592
+ }
1593
+ /**
1594
+ * Send a message to the React app via WebSocket.
1595
+ * Used by the MCP's DevToolsBridge to send messages.
1596
+ */
1597
+ sendMessage(event, payload) {
1598
+ if (this._socket && this._socket.readyState === WebSocket2.OPEN) {
1599
+ this._socket.send(JSON.stringify({ event, payload }));
1600
+ }
1601
+ }
1602
+ /**
1603
+ * Start the headless DevTools server
1604
+ */
1605
+ async start() {
1606
+ if (this._state.status === "listening" || this._state.status === "connected") {
1607
+ this._options.logger.debug("Server already running");
1608
+ return;
1609
+ }
1610
+ this._setState({ status: "starting", error: null });
1611
+ const { port, host, httpsOptions } = this._options;
1612
+ const logger = this._options.logger;
1613
+ return new Promise((resolve, reject) => {
1614
+ try {
1615
+ this._httpServer = httpsOptions ? createHttpsServer(httpsOptions) : createHttpServer();
1616
+ this._httpServer.on("request", (req, res) => {
1617
+ this._handleHttpRequest(req, res);
1618
+ });
1619
+ this._wsServer = new WebSocketServer({
1620
+ server: this._httpServer,
1621
+ maxPayload: 1e9
1622
+ // 1GB - same as standalone.js
1623
+ });
1624
+ this._wsServer.on("connection", (socket) => {
1625
+ this._handleConnection(socket);
1626
+ });
1627
+ this._wsServer.on("error", (err) => {
1628
+ logger.error("WebSocket server error", { error: err.message });
1629
+ this._setState({ status: "error", error: err.message });
1630
+ this.emit("error", err);
1631
+ });
1632
+ this._httpServer.on("error", (err) => {
1633
+ logger.error("HTTP server error", { error: err.message, code: err.code });
1634
+ if (err.code === "EADDRINUSE") {
1635
+ this._setState({
1636
+ status: "error",
1637
+ error: `Port ${port} is already in use. Another DevTools instance may be running.`
1638
+ });
1639
+ } else {
1640
+ this._setState({ status: "error", error: err.message });
1641
+ }
1642
+ this.emit("error", err);
1643
+ reject(err);
1644
+ });
1645
+ this._httpServer.listen(port, host, () => {
1646
+ logger.info("Headless DevTools server listening", { port, host });
1647
+ this._setState({ status: "listening" });
1648
+ this.emit("listening", { port, host });
1649
+ resolve();
1650
+ });
1651
+ } catch (err) {
1652
+ const message = err instanceof Error ? err.message : "Unknown error";
1653
+ logger.error("Failed to start server", { error: message });
1654
+ this._setState({ status: "error", error: message });
1655
+ reject(err);
1656
+ }
1657
+ });
1658
+ }
1659
+ /**
1660
+ * Stop the server
1661
+ */
1662
+ async stop() {
1663
+ const logger = this._options.logger;
1664
+ logger.info("Stopping headless DevTools server");
1665
+ if (this._socket) {
1666
+ this._socket.close();
1667
+ this._socket = null;
1668
+ }
1669
+ if (this._wsServer) {
1670
+ this._wsServer.close();
1671
+ this._wsServer = null;
1672
+ }
1673
+ if (this._httpServer) {
1674
+ this._httpServer.close();
1675
+ this._httpServer = null;
1676
+ }
1677
+ this._setState({
1678
+ status: "stopped",
1679
+ connectedAt: null,
1680
+ error: null
1681
+ });
1682
+ this.emit("stopped");
1683
+ }
1684
+ /**
1685
+ * Handle HTTP requests - serve backend.js for web apps
1686
+ */
1687
+ _handleHttpRequest(_req, res) {
1688
+ const { port, host, httpsOptions, logger } = this._options;
1689
+ const useHttps = !!httpsOptions;
1690
+ if (!this._backendScript) {
1691
+ logger.warn("Backend script not available");
1692
+ res.writeHead(503);
1693
+ res.end("Backend script not available. Web apps need to include react-devtools backend manually.");
1694
+ return;
1695
+ }
1696
+ logger.debug("Serving backend.js to web client");
1697
+ const responseScript = `${this._backendScript}
1698
+ ;ReactDevToolsBackend.initialize();
1699
+ ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${useHttps}});
1700
+ `;
1701
+ res.end(responseScript);
1702
+ }
1703
+ /**
1704
+ * Handle new WebSocket connection from React app
1705
+ */
1706
+ _handleConnection(socket) {
1707
+ const logger = this._options.logger;
1708
+ if (this._socket !== null) {
1709
+ logger.warn("Only one connection allowed at a time. Closing previous connection.");
1710
+ this._socket.close();
1711
+ }
1712
+ logger.info("React app connected");
1713
+ this._socket = socket;
1714
+ socket.on("message", (data) => {
1715
+ try {
1716
+ const message = JSON.parse(data.toString());
1717
+ logger.debug("Received message", { event: message.event });
1718
+ this._externalMessageListeners.forEach((fn) => {
1719
+ try {
1720
+ fn(message.event, message.payload);
1721
+ } catch (err) {
1722
+ logger.error("Error in external message listener", { error: err instanceof Error ? err.message : "Unknown" });
1723
+ }
1724
+ });
1725
+ } catch (err) {
1726
+ logger.error("Failed to parse message", { data: data.toString().slice(0, 100) });
1727
+ }
1728
+ });
1729
+ socket.on("close", () => {
1730
+ logger.info("React app disconnected");
1731
+ this._onDisconnected();
1732
+ });
1733
+ socket.on("error", (err) => {
1734
+ logger.error("WebSocket connection error", { error: err.message });
1735
+ this._onDisconnected();
1736
+ });
1737
+ this._setState({
1738
+ status: "connected",
1739
+ connectedAt: Date.now()
1740
+ });
1741
+ this.emit("connected");
1742
+ }
1743
+ /**
1744
+ * Handle disconnection
1745
+ */
1746
+ _onDisconnected() {
1747
+ this._socket = null;
1748
+ this._setState({
1749
+ status: "listening",
1750
+ connectedAt: null
1751
+ });
1752
+ this.emit("disconnected");
1753
+ }
1754
+ _setState(updates) {
1755
+ this._state = { ...this._state, ...updates };
1756
+ this.emit("stateChange", this._state);
1757
+ }
1758
+ };
1759
+ async function startHeadlessServer(options = {}) {
1760
+ const server = new HeadlessDevToolsServer(options);
1761
+ await server.start();
1762
+ return server;
1763
+ }
1764
+
1765
+ // src/server.ts
1766
+ var TOOLS = [
1767
+ // Connection
1768
+ {
1769
+ name: "connect",
1770
+ description: "Connect to React DevTools backend via WebSocket",
1771
+ inputSchema: {
1772
+ type: "object",
1773
+ properties: {
1774
+ host: { type: "string", description: "Host (default: localhost)" },
1775
+ port: { type: "number", description: "Port (default: 8097)" },
1776
+ timeout: { type: "number", description: "Timeout in ms (default: 5000)" }
1777
+ }
1778
+ }
1779
+ },
1780
+ {
1781
+ name: "disconnect",
1782
+ description: "Disconnect from React DevTools backend",
1783
+ inputSchema: { type: "object", properties: {} }
1784
+ },
1785
+ {
1786
+ name: "get_connection_status",
1787
+ description: "Get current connection status",
1788
+ inputSchema: { type: "object", properties: {} }
1789
+ },
1790
+ // Component Tree
1791
+ {
1792
+ name: "get_component_tree",
1793
+ description: "Get the React component tree for all roots",
1794
+ inputSchema: {
1795
+ type: "object",
1796
+ properties: {
1797
+ rootID: { type: "number", description: "Filter by root ID (optional)" },
1798
+ maxDepth: { type: "number", description: "Maximum depth to return (optional)" }
1799
+ }
1800
+ }
1801
+ },
1802
+ {
1803
+ name: "get_element_by_id",
1804
+ description: "Get basic element info by ID",
1805
+ inputSchema: {
1806
+ type: "object",
1807
+ properties: {
1808
+ id: { type: "number", description: "Element ID" }
1809
+ },
1810
+ required: ["id"]
1811
+ }
1812
+ },
1813
+ {
1814
+ name: "search_components",
1815
+ description: "Search for components by name",
1816
+ inputSchema: {
1817
+ type: "object",
1818
+ properties: {
1819
+ query: { type: "string", description: "Search query (component name)" },
1820
+ caseSensitive: { type: "boolean", description: "Case sensitive (default: false)" },
1821
+ isRegex: { type: "boolean", description: "Regex search (default: false)" }
1822
+ },
1823
+ required: ["query"]
1824
+ }
1825
+ },
1826
+ // Inspection
1827
+ {
1828
+ name: "inspect_element",
1829
+ description: "Get full inspection data for a component including props, state, hooks",
1830
+ inputSchema: {
1831
+ type: "object",
1832
+ properties: {
1833
+ id: { type: "number", description: "Element ID to inspect" },
1834
+ paths: {
1835
+ type: "array",
1836
+ items: {
1837
+ type: "array",
1838
+ items: { oneOf: [{ type: "string" }, { type: "number" }] }
1839
+ },
1840
+ description: "Paths to hydrate for lazy loading"
1841
+ }
1842
+ },
1843
+ required: ["id"]
1844
+ }
1845
+ },
1846
+ {
1847
+ name: "get_owners_list",
1848
+ description: "Get the chain of components that rendered this element",
1849
+ inputSchema: {
1850
+ type: "object",
1851
+ properties: {
1852
+ id: { type: "number", description: "Element ID" }
1853
+ },
1854
+ required: ["id"]
1855
+ }
1856
+ },
1857
+ {
1858
+ name: "get_element_source",
1859
+ description: "Get source location for an element",
1860
+ inputSchema: {
1861
+ type: "object",
1862
+ properties: {
1863
+ id: { type: "number", description: "Element ID" }
1864
+ },
1865
+ required: ["id"]
1866
+ }
1867
+ },
1868
+ // Overrides
1869
+ {
1870
+ name: "override_props",
1871
+ description: "Override a prop value on a component",
1872
+ inputSchema: {
1873
+ type: "object",
1874
+ properties: {
1875
+ id: { type: "number", description: "Element ID" },
1876
+ path: {
1877
+ type: "array",
1878
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
1879
+ description: "Path to the prop"
1880
+ },
1881
+ value: { description: "New value" }
1882
+ },
1883
+ required: ["id", "path", "value"]
1884
+ }
1885
+ },
1886
+ {
1887
+ name: "override_state",
1888
+ description: "Override a state value on a class component",
1889
+ inputSchema: {
1890
+ type: "object",
1891
+ properties: {
1892
+ id: { type: "number", description: "Element ID" },
1893
+ path: {
1894
+ type: "array",
1895
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
1896
+ description: "Path to state key"
1897
+ },
1898
+ value: { description: "New value" }
1899
+ },
1900
+ required: ["id", "path", "value"]
1901
+ }
1902
+ },
1903
+ {
1904
+ name: "override_hooks",
1905
+ description: "Override a hook value on a function component",
1906
+ inputSchema: {
1907
+ type: "object",
1908
+ properties: {
1909
+ id: { type: "number", description: "Element ID" },
1910
+ hookIndex: { type: "number", description: "Hook index" },
1911
+ path: {
1912
+ type: "array",
1913
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
1914
+ description: "Path within hook value"
1915
+ },
1916
+ value: { description: "New value" }
1917
+ },
1918
+ required: ["id", "hookIndex", "path", "value"]
1919
+ }
1920
+ },
1921
+ {
1922
+ name: "override_context",
1923
+ description: "Override a context value",
1924
+ inputSchema: {
1925
+ type: "object",
1926
+ properties: {
1927
+ id: { type: "number", description: "Element ID" },
1928
+ path: {
1929
+ type: "array",
1930
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
1931
+ description: "Path within context"
1932
+ },
1933
+ value: { description: "New value" }
1934
+ },
1935
+ required: ["id", "path", "value"]
1936
+ }
1937
+ },
1938
+ {
1939
+ name: "delete_path",
1940
+ description: "Delete a path from props/state/hooks/context",
1941
+ inputSchema: {
1942
+ type: "object",
1943
+ properties: {
1944
+ id: { type: "number", description: "Element ID" },
1945
+ target: { type: "string", enum: ["props", "state", "hooks", "context"], description: "Target" },
1946
+ hookIndex: { type: "number", description: "Hook index (if target is hooks)" },
1947
+ path: {
1948
+ type: "array",
1949
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
1950
+ description: "Path to delete"
1951
+ }
1952
+ },
1953
+ required: ["id", "target", "path"]
1954
+ }
1955
+ },
1956
+ {
1957
+ name: "rename_path",
1958
+ description: "Rename a key in props/state/hooks/context",
1959
+ inputSchema: {
1960
+ type: "object",
1961
+ properties: {
1962
+ id: { type: "number", description: "Element ID" },
1963
+ target: { type: "string", enum: ["props", "state", "hooks", "context"], description: "Target" },
1964
+ hookIndex: { type: "number", description: "Hook index (if target is hooks)" },
1965
+ path: {
1966
+ type: "array",
1967
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
1968
+ description: "Path to the key"
1969
+ },
1970
+ oldKey: { type: "string", description: "Old key name" },
1971
+ newKey: { type: "string", description: "New key name" }
1972
+ },
1973
+ required: ["id", "target", "path", "oldKey", "newKey"]
1974
+ }
1975
+ },
1976
+ // Profiling
1977
+ {
1978
+ name: "start_profiling",
1979
+ description: "Start profiling React renders",
1980
+ inputSchema: {
1981
+ type: "object",
1982
+ properties: {
1983
+ recordTimeline: { type: "boolean", description: "Record timeline data" },
1984
+ recordChangeDescriptions: { type: "boolean", description: "Record why components rendered" }
1985
+ }
1986
+ }
1987
+ },
1988
+ {
1989
+ name: "stop_profiling",
1990
+ description: "Stop profiling and get data",
1991
+ inputSchema: { type: "object", properties: {} }
1992
+ },
1993
+ {
1994
+ name: "get_profiling_data",
1995
+ description: "Get profiling data without stopping",
1996
+ inputSchema: { type: "object", properties: {} }
1997
+ },
1998
+ {
1999
+ name: "get_profiling_status",
2000
+ description: "Check if profiling is active",
2001
+ inputSchema: { type: "object", properties: {} }
2002
+ },
2003
+ // Error & Suspense
2004
+ {
2005
+ name: "get_errors_and_warnings",
2006
+ description: "Get all errors and warnings from components",
2007
+ inputSchema: { type: "object", properties: {} }
2008
+ },
2009
+ {
2010
+ name: "clear_errors_and_warnings",
2011
+ description: "Clear all or specific element's errors/warnings",
2012
+ inputSchema: {
2013
+ type: "object",
2014
+ properties: {
2015
+ id: { type: "number", description: "Element ID (optional, clears all if omitted)" },
2016
+ clearErrors: { type: "boolean", description: "Clear errors" },
2017
+ clearWarnings: { type: "boolean", description: "Clear warnings" }
2018
+ }
2019
+ }
2020
+ },
2021
+ {
2022
+ name: "toggle_error",
2023
+ description: "Toggle error boundary state for testing",
2024
+ inputSchema: {
2025
+ type: "object",
2026
+ properties: {
2027
+ id: { type: "number", description: "Element ID" },
2028
+ isErrored: { type: "boolean", description: "Force error state" }
2029
+ },
2030
+ required: ["id", "isErrored"]
2031
+ }
2032
+ },
2033
+ {
2034
+ name: "toggle_suspense",
2035
+ description: "Toggle suspense state for testing",
2036
+ inputSchema: {
2037
+ type: "object",
2038
+ properties: {
2039
+ id: { type: "number", description: "Element ID" },
2040
+ isSuspended: { type: "boolean", description: "Force suspended state" }
2041
+ },
2042
+ required: ["id", "isSuspended"]
2043
+ }
2044
+ },
2045
+ // Debugging
2046
+ {
2047
+ name: "highlight_element",
2048
+ description: "Highlight an element in the app UI",
2049
+ inputSchema: {
2050
+ type: "object",
2051
+ properties: {
2052
+ id: { type: "number", description: "Element ID to highlight" },
2053
+ duration: { type: "number", description: "Highlight duration in ms (default: 2000)" }
2054
+ },
2055
+ required: ["id"]
2056
+ }
2057
+ },
2058
+ {
2059
+ name: "clear_highlight",
2060
+ description: "Clear any active element highlight",
2061
+ inputSchema: { type: "object", properties: {} }
2062
+ },
2063
+ {
2064
+ name: "scroll_to_element",
2065
+ description: "Scroll the app to show an element",
2066
+ inputSchema: {
2067
+ type: "object",
2068
+ properties: {
2069
+ id: { type: "number", description: "Element ID" }
2070
+ },
2071
+ required: ["id"]
2072
+ }
2073
+ },
2074
+ {
2075
+ name: "log_to_console",
2076
+ description: "Log an element to the browser/app console as $r",
2077
+ inputSchema: {
2078
+ type: "object",
2079
+ properties: {
2080
+ id: { type: "number", description: "Element ID" }
2081
+ },
2082
+ required: ["id"]
2083
+ }
2084
+ },
2085
+ {
2086
+ name: "store_as_global",
2087
+ description: "Store a value as a global variable for console access",
2088
+ inputSchema: {
2089
+ type: "object",
2090
+ properties: {
2091
+ id: { type: "number", description: "Element ID" },
2092
+ path: {
2093
+ type: "array",
2094
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
2095
+ description: "Path to the value"
2096
+ },
2097
+ globalName: { type: "string", description: "Global variable name" }
2098
+ },
2099
+ required: ["id", "path", "globalName"]
2100
+ }
2101
+ },
2102
+ {
2103
+ name: "view_source",
2104
+ description: "Open element source in IDE (if supported)",
2105
+ inputSchema: {
2106
+ type: "object",
2107
+ properties: {
2108
+ id: { type: "number", description: "Element ID" }
2109
+ },
2110
+ required: ["id"]
2111
+ }
2112
+ },
2113
+ // Filters
2114
+ {
2115
+ name: "get_component_filters",
2116
+ description: "Get current component filters",
2117
+ inputSchema: { type: "object", properties: {} }
2118
+ },
2119
+ {
2120
+ name: "set_component_filters",
2121
+ description: "Set component filters (hide certain components)",
2122
+ inputSchema: {
2123
+ type: "object",
2124
+ properties: {
2125
+ filters: {
2126
+ type: "array",
2127
+ items: {
2128
+ type: "object",
2129
+ properties: {
2130
+ type: { type: "string", enum: ["name", "location", "type", "hoc"] },
2131
+ value: { type: "string" },
2132
+ isEnabled: { type: "boolean" },
2133
+ isRegex: { type: "boolean" }
2134
+ },
2135
+ required: ["type", "value", "isEnabled"]
2136
+ }
2137
+ }
2138
+ },
2139
+ required: ["filters"]
2140
+ }
2141
+ },
2142
+ {
2143
+ name: "set_trace_updates_enabled",
2144
+ description: "Enable/disable visual update highlighting",
2145
+ inputSchema: {
2146
+ type: "object",
2147
+ properties: {
2148
+ enabled: { type: "boolean", description: "Enable trace updates" }
2149
+ },
2150
+ required: ["enabled"]
2151
+ }
2152
+ },
2153
+ // React Native
2154
+ {
2155
+ name: "get_native_style",
2156
+ description: "Get native style and layout info (React Native only)",
2157
+ inputSchema: {
2158
+ type: "object",
2159
+ properties: {
2160
+ id: { type: "number", description: "Element ID" }
2161
+ },
2162
+ required: ["id"]
2163
+ }
2164
+ },
2165
+ {
2166
+ name: "set_native_style",
2167
+ description: "Set a native style property (React Native only)",
2168
+ inputSchema: {
2169
+ type: "object",
2170
+ properties: {
2171
+ id: { type: "number", description: "Element ID" },
2172
+ property: { type: "string", description: "Style property name" },
2173
+ value: { description: "New value" }
2174
+ },
2175
+ required: ["id", "property", "value"]
2176
+ }
2177
+ },
2178
+ // Health & Monitoring
2179
+ {
2180
+ name: "health_check",
2181
+ description: "Get server and connection health status",
2182
+ inputSchema: { type: "object", properties: {} }
2183
+ },
2184
+ // Phase 2: Protocol & Renderer Management
2185
+ {
2186
+ name: "get_capabilities",
2187
+ description: "Get negotiated protocol capabilities (features supported by backend)",
2188
+ inputSchema: { type: "object", properties: {} }
2189
+ },
2190
+ {
2191
+ name: "get_renderers",
2192
+ description: "Get all connected React renderers (for multi-renderer apps)",
2193
+ inputSchema: { type: "object", properties: {} }
2194
+ },
2195
+ {
2196
+ name: "get_renderer",
2197
+ description: "Get a specific renderer by ID",
2198
+ inputSchema: {
2199
+ type: "object",
2200
+ properties: {
2201
+ id: { type: "number", description: "Renderer ID" }
2202
+ },
2203
+ required: ["id"]
2204
+ }
2205
+ },
2206
+ {
2207
+ name: "get_elements_by_renderer",
2208
+ description: "Get all elements for a specific renderer",
2209
+ inputSchema: {
2210
+ type: "object",
2211
+ properties: {
2212
+ rendererID: { type: "number", description: "Renderer ID" }
2213
+ },
2214
+ required: ["rendererID"]
2215
+ }
2216
+ },
2217
+ // Phase 2: Native Inspection
2218
+ {
2219
+ name: "start_inspecting_native",
2220
+ description: "Start native element inspection mode (tap-to-select)",
2221
+ inputSchema: { type: "object", properties: {} }
2222
+ },
2223
+ {
2224
+ name: "stop_inspecting_native",
2225
+ description: "Stop native element inspection mode",
2226
+ inputSchema: {
2227
+ type: "object",
2228
+ properties: {
2229
+ selectNextElement: { type: "boolean", description: "Select element under pointer (default: true)" }
2230
+ }
2231
+ }
2232
+ },
2233
+ {
2234
+ name: "get_inspecting_native_status",
2235
+ description: "Check if native inspection mode is active",
2236
+ inputSchema: { type: "object", properties: {} }
2237
+ },
2238
+ // Phase 2: Additional Features
2239
+ {
2240
+ name: "capture_screenshot",
2241
+ description: "Capture screenshot of an element (if supported)",
2242
+ inputSchema: {
2243
+ type: "object",
2244
+ properties: {
2245
+ id: { type: "number", description: "Element ID" }
2246
+ },
2247
+ required: ["id"]
2248
+ }
2249
+ },
2250
+ {
2251
+ name: "save_to_clipboard",
2252
+ description: "Save content to system clipboard",
2253
+ inputSchema: {
2254
+ type: "object",
2255
+ properties: {
2256
+ value: { type: "string", description: "Content to save" }
2257
+ },
2258
+ required: ["value"]
2259
+ }
2260
+ },
2261
+ {
2262
+ name: "view_attribute_source",
2263
+ description: "Get source location for a specific attribute path",
2264
+ inputSchema: {
2265
+ type: "object",
2266
+ properties: {
2267
+ id: { type: "number", description: "Element ID" },
2268
+ path: {
2269
+ type: "array",
2270
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
2271
+ description: "Path to attribute"
2272
+ }
2273
+ },
2274
+ required: ["id", "path"]
2275
+ }
2276
+ }
2277
+ ];
2278
+ function createServer(options = {}) {
2279
+ const logger = options.logger ?? createLogger({
2280
+ level: getLogLevelFromEnv(),
2281
+ prefix: "devtools-mcp"
2282
+ });
2283
+ const host = options.host ?? process.env.DEVTOOLS_HOST ?? "localhost";
2284
+ const port = options.port ?? (Number(process.env.DEVTOOLS_PORT) || 8097);
2285
+ const standalone = options.standalone ?? process.env.DEVTOOLS_STANDALONE !== "false";
2286
+ let headlessServer = null;
2287
+ const bridge = new DevToolsBridge({
2288
+ host,
2289
+ port,
2290
+ timeout: Number(process.env.DEVTOOLS_TIMEOUT) || 5e3,
2291
+ logger: logger.child("bridge")
2292
+ });
2293
+ const server = new Server(
2294
+ {
2295
+ name: "react-devtools-mcp",
2296
+ version: "0.1.0"
2297
+ },
2298
+ {
2299
+ capabilities: {
2300
+ tools: {},
2301
+ resources: {}
2302
+ }
2303
+ }
2304
+ );
2305
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2306
+ tools: TOOLS
2307
+ }));
2308
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
2309
+ resources: [
2310
+ {
2311
+ uri: "devtools://components",
2312
+ name: "Component Tree",
2313
+ description: "Live component tree updates",
2314
+ mimeType: "application/json"
2315
+ },
2316
+ {
2317
+ uri: "devtools://selection",
2318
+ name: "Current Selection",
2319
+ description: "Currently selected element",
2320
+ mimeType: "application/json"
2321
+ }
2322
+ ]
2323
+ }));
2324
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2325
+ const uri = request.params.uri;
2326
+ if (uri === "devtools://components") {
2327
+ const tree = bridge.getComponentTree();
2328
+ return {
2329
+ contents: [
2330
+ {
2331
+ uri,
2332
+ mimeType: "application/json",
2333
+ text: JSON.stringify(tree, null, 2)
2334
+ }
2335
+ ]
2336
+ };
2337
+ }
2338
+ if (uri === "devtools://selection") {
2339
+ return {
2340
+ contents: [
2341
+ {
2342
+ uri,
2343
+ mimeType: "application/json",
2344
+ text: JSON.stringify({ selectedElementID: null })
2345
+ }
2346
+ ]
2347
+ };
2348
+ }
2349
+ throw new Error(`Unknown resource: ${uri}`);
2350
+ });
2351
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2352
+ const { name, arguments: args } = request.params;
2353
+ try {
2354
+ const result = await handleToolCall(bridge, name, args ?? {});
2355
+ return {
2356
+ content: [
2357
+ {
2358
+ type: "text",
2359
+ text: JSON.stringify(result, null, 2)
2360
+ }
2361
+ ]
2362
+ };
2363
+ } catch (error) {
2364
+ const message = error instanceof Error ? error.message : "Unknown error";
2365
+ return {
2366
+ content: [
2367
+ {
2368
+ type: "text",
2369
+ text: JSON.stringify({ error: message })
2370
+ }
2371
+ ],
2372
+ isError: true
2373
+ };
2374
+ }
2375
+ });
2376
+ const autoConnect = options.autoConnect ?? process.env.DEVTOOLS_AUTO_CONNECT !== "false";
2377
+ return {
2378
+ server,
2379
+ bridge,
2380
+ headlessServer: () => headlessServer,
2381
+ async start() {
2382
+ if (standalone) {
2383
+ try {
2384
+ logger.info("Starting standalone mode with embedded DevTools server", { host, port });
2385
+ headlessServer = await startHeadlessServer({
2386
+ host,
2387
+ port,
2388
+ logger: logger.child("headless")
2389
+ });
2390
+ let bridgeHandle = null;
2391
+ headlessServer.addMessageListener((event, payload) => {
2392
+ if (bridgeHandle) {
2393
+ const data = JSON.stringify({ event, payload });
2394
+ bridgeHandle.receiveMessage(data);
2395
+ } else {
2396
+ logger.debug("Message received before bridge attached", { event });
2397
+ }
2398
+ });
2399
+ headlessServer.on("connected", () => {
2400
+ logger.info("React app connected to embedded DevTools server");
2401
+ bridgeHandle = bridge.attachToExternal(
2402
+ (event, payload) => {
2403
+ headlessServer.sendMessage(event, payload);
2404
+ },
2405
+ () => {
2406
+ logger.info("MCP bridge detached from headless server");
2407
+ }
2408
+ );
2409
+ });
2410
+ headlessServer.on("disconnected", () => {
2411
+ logger.info("React app disconnected from embedded DevTools server");
2412
+ bridgeHandle?.detach();
2413
+ bridgeHandle = null;
2414
+ });
2415
+ headlessServer.on("error", (err) => {
2416
+ logger.error("Embedded DevTools server error", { error: err.message });
2417
+ });
2418
+ } catch (err) {
2419
+ const message = err instanceof Error ? err.message : "Unknown error";
2420
+ logger.error("Failed to start embedded DevTools server", { error: message });
2421
+ }
2422
+ }
2423
+ const transport = new StdioServerTransport();
2424
+ await server.connect(transport);
2425
+ if (autoConnect && !standalone) {
2426
+ try {
2427
+ await bridge.connect();
2428
+ } catch {
2429
+ }
2430
+ }
2431
+ },
2432
+ async stop() {
2433
+ if (headlessServer) {
2434
+ await headlessServer.stop();
2435
+ headlessServer = null;
2436
+ }
2437
+ bridge.disconnect();
2438
+ }
2439
+ };
2440
+ }
2441
+ async function handleToolCall(bridge, name, args) {
2442
+ switch (name) {
2443
+ // Connection
2444
+ case "connect": {
2445
+ const status = await bridge.connect();
2446
+ return { success: true, status };
2447
+ }
2448
+ case "disconnect": {
2449
+ bridge.disconnect();
2450
+ return { success: true };
2451
+ }
2452
+ case "get_connection_status": {
2453
+ return { status: bridge.getStatus() };
2454
+ }
2455
+ // Component Tree
2456
+ case "get_component_tree": {
2457
+ const roots = bridge.getComponentTree(
2458
+ args.rootID,
2459
+ args.maxDepth
2460
+ );
2461
+ return { roots };
2462
+ }
2463
+ case "get_element_by_id": {
2464
+ const element = bridge.getElementById(args.id);
2465
+ return { element };
2466
+ }
2467
+ case "search_components": {
2468
+ const matches = bridge.searchComponents(
2469
+ args.query,
2470
+ args.caseSensitive,
2471
+ args.isRegex
2472
+ );
2473
+ return { matches, totalCount: matches.length };
2474
+ }
2475
+ // Inspection
2476
+ case "inspect_element": {
2477
+ const result = await bridge.inspectElement(
2478
+ args.id,
2479
+ args.paths
2480
+ );
2481
+ if (result.type === "full-data") {
2482
+ return { success: true, element: result.element, error: null };
2483
+ } else if (result.type === "not-found") {
2484
+ return { success: false, element: null, error: { type: "not_found", message: "Element not found" } };
2485
+ } else if (result.type === "error") {
2486
+ return { success: false, element: null, error: { type: result.errorType, message: result.message, stack: result.stack } };
2487
+ } else {
2488
+ return { success: true, element: null, error: null };
2489
+ }
2490
+ }
2491
+ case "get_owners_list": {
2492
+ const owners = await bridge.getOwnersList(args.id);
2493
+ return { owners };
2494
+ }
2495
+ case "get_element_source": {
2496
+ const result = await bridge.inspectElement(args.id);
2497
+ if (result.type === "full-data") {
2498
+ return { source: result.element.source, stack: result.element.stack };
2499
+ }
2500
+ return { source: null, stack: null };
2501
+ }
2502
+ // Overrides
2503
+ case "override_props": {
2504
+ bridge.overrideValueAtPath(
2505
+ "props",
2506
+ args.id,
2507
+ args.path,
2508
+ args.value
2509
+ );
2510
+ return { success: true };
2511
+ }
2512
+ case "override_state": {
2513
+ bridge.overrideValueAtPath(
2514
+ "state",
2515
+ args.id,
2516
+ args.path,
2517
+ args.value
2518
+ );
2519
+ return { success: true };
2520
+ }
2521
+ case "override_hooks": {
2522
+ bridge.overrideValueAtPath(
2523
+ "hooks",
2524
+ args.id,
2525
+ args.path,
2526
+ args.value,
2527
+ args.hookIndex
2528
+ );
2529
+ return { success: true };
2530
+ }
2531
+ case "override_context": {
2532
+ bridge.overrideValueAtPath(
2533
+ "context",
2534
+ args.id,
2535
+ args.path,
2536
+ args.value
2537
+ );
2538
+ return { success: true };
2539
+ }
2540
+ case "delete_path": {
2541
+ bridge.deletePath(
2542
+ args.target,
2543
+ args.id,
2544
+ args.path,
2545
+ args.hookIndex
2546
+ );
2547
+ return { success: true };
2548
+ }
2549
+ case "rename_path": {
2550
+ bridge.renamePath(
2551
+ args.target,
2552
+ args.id,
2553
+ args.path,
2554
+ args.oldKey,
2555
+ args.newKey,
2556
+ args.hookIndex
2557
+ );
2558
+ return { success: true };
2559
+ }
2560
+ // Profiling
2561
+ case "start_profiling": {
2562
+ bridge.startProfiling(
2563
+ args.recordTimeline,
2564
+ args.recordChangeDescriptions
2565
+ );
2566
+ return { success: true, requiresReload: false };
2567
+ }
2568
+ case "stop_profiling": {
2569
+ bridge.stopProfiling();
2570
+ const data = await bridge.getProfilingData();
2571
+ return { success: true, data };
2572
+ }
2573
+ case "get_profiling_data": {
2574
+ const status = bridge.getProfilingStatus();
2575
+ const data = await bridge.getProfilingData();
2576
+ return { isActive: status.isProfiling, data };
2577
+ }
2578
+ case "get_profiling_status": {
2579
+ const status = bridge.getProfilingStatus();
2580
+ return {
2581
+ isProfiling: status.isProfiling,
2582
+ recordTimeline: false,
2583
+ recordChangeDescriptions: true
2584
+ };
2585
+ }
2586
+ // Error & Suspense
2587
+ case "get_errors_and_warnings": {
2588
+ const { errors, warnings } = bridge.getErrorsAndWarnings();
2589
+ return {
2590
+ errors: Object.fromEntries(errors),
2591
+ warnings: Object.fromEntries(warnings)
2592
+ };
2593
+ }
2594
+ case "clear_errors_and_warnings": {
2595
+ bridge.clearErrorsAndWarnings(args.id);
2596
+ return { success: true };
2597
+ }
2598
+ case "toggle_error": {
2599
+ bridge.overrideError(args.id, args.isErrored);
2600
+ return { success: true };
2601
+ }
2602
+ case "toggle_suspense": {
2603
+ bridge.overrideSuspense(args.id, args.isSuspended);
2604
+ return { success: true };
2605
+ }
2606
+ // Debugging
2607
+ case "highlight_element": {
2608
+ bridge.highlightElement(args.id);
2609
+ const duration = args.duration ?? 2e3;
2610
+ setTimeout(() => bridge.clearHighlight(), duration);
2611
+ return { success: true };
2612
+ }
2613
+ case "clear_highlight": {
2614
+ bridge.clearHighlight();
2615
+ return { success: true };
2616
+ }
2617
+ case "scroll_to_element": {
2618
+ bridge.scrollToElement(args.id);
2619
+ return { success: true };
2620
+ }
2621
+ case "log_to_console": {
2622
+ bridge.logToConsole(args.id);
2623
+ return { success: true };
2624
+ }
2625
+ case "store_as_global": {
2626
+ bridge.storeAsGlobal(
2627
+ args.id,
2628
+ args.path,
2629
+ 1
2630
+ // count
2631
+ );
2632
+ return { success: true };
2633
+ }
2634
+ case "view_source": {
2635
+ bridge.viewElementSource(args.id);
2636
+ const result = await bridge.inspectElement(args.id);
2637
+ if (result.type === "full-data") {
2638
+ return { success: true, source: result.element.source };
2639
+ }
2640
+ return { success: true, source: null };
2641
+ }
2642
+ // Filters
2643
+ case "get_component_filters": {
2644
+ return { filters: [] };
2645
+ }
2646
+ case "set_component_filters": {
2647
+ bridge.setComponentFilters(args.filters);
2648
+ return { success: true };
2649
+ }
2650
+ case "set_trace_updates_enabled": {
2651
+ bridge.setTraceUpdatesEnabled(args.enabled);
2652
+ return { success: true };
2653
+ }
2654
+ // React Native
2655
+ case "get_native_style": {
2656
+ const result = await bridge.getNativeStyle(args.id);
2657
+ return result;
2658
+ }
2659
+ case "set_native_style": {
2660
+ bridge.setNativeStyle(
2661
+ args.id,
2662
+ args.property,
2663
+ args.value
2664
+ );
2665
+ return { success: true };
2666
+ }
2667
+ // Health & Monitoring
2668
+ case "health_check": {
2669
+ const status = bridge.getStatus();
2670
+ const lastMessageTime = bridge.getLastMessageTime();
2671
+ const pendingRequests = bridge.getPendingRequestCount();
2672
+ const now = Date.now();
2673
+ return {
2674
+ connected: status.state === "connected",
2675
+ state: status.state,
2676
+ rendererCount: status.rendererCount,
2677
+ reactVersion: status.reactVersion,
2678
+ error: status.error,
2679
+ lastMessageAgo: lastMessageTime > 0 ? now - lastMessageTime : null,
2680
+ pendingRequests,
2681
+ uptime: process.uptime()
2682
+ };
2683
+ }
2684
+ // Phase 2: Protocol & Renderer Management
2685
+ case "get_capabilities": {
2686
+ const capabilities = bridge.getCapabilities();
2687
+ const negotiated = bridge.hasNegotiatedCapabilities();
2688
+ return { capabilities, negotiated };
2689
+ }
2690
+ case "get_renderers": {
2691
+ const renderers = bridge.getRenderers();
2692
+ return {
2693
+ renderers: renderers.map((r) => ({
2694
+ id: r.id,
2695
+ version: r.version,
2696
+ packageName: r.packageName,
2697
+ rootCount: r.rootIDs.size,
2698
+ elementCount: r.elementIDs.size
2699
+ }))
2700
+ };
2701
+ }
2702
+ case "get_renderer": {
2703
+ const renderer = bridge.getRenderer(args.id);
2704
+ if (!renderer) {
2705
+ return { renderer: null };
2706
+ }
2707
+ return {
2708
+ renderer: {
2709
+ id: renderer.id,
2710
+ version: renderer.version,
2711
+ packageName: renderer.packageName,
2712
+ rootIDs: Array.from(renderer.rootIDs),
2713
+ elementCount: renderer.elementIDs.size
2714
+ }
2715
+ };
2716
+ }
2717
+ case "get_elements_by_renderer": {
2718
+ const elements = bridge.getElementsByRenderer(args.rendererID);
2719
+ return { elements, count: elements.length };
2720
+ }
2721
+ // Phase 2: Native Inspection
2722
+ case "start_inspecting_native": {
2723
+ bridge.startInspectingNative();
2724
+ return { success: true, isInspecting: true };
2725
+ }
2726
+ case "stop_inspecting_native": {
2727
+ const selectNextElement = args.selectNextElement !== false;
2728
+ const elementID = await bridge.stopInspectingNative(selectNextElement);
2729
+ return { success: true, selectedElementID: elementID };
2730
+ }
2731
+ case "get_inspecting_native_status": {
2732
+ return { isInspecting: bridge.isInspectingNativeMode() };
2733
+ }
2734
+ // Phase 2: Additional Features
2735
+ case "capture_screenshot": {
2736
+ const screenshot = await bridge.captureScreenshot(args.id);
2737
+ return { success: screenshot !== null, screenshot };
2738
+ }
2739
+ case "save_to_clipboard": {
2740
+ const clipResult = await bridge.saveToClipboard(args.value);
2741
+ return clipResult;
2742
+ }
2743
+ case "view_attribute_source": {
2744
+ const source = await bridge.viewAttributeSource(
2745
+ args.id,
2746
+ args.path
2747
+ );
2748
+ return { source };
2749
+ }
2750
+ default:
2751
+ throw new Error(`Unknown tool: ${name}`);
2752
+ }
2753
+ }
2754
+
2755
+ // src/cli.ts
2756
+ async function main() {
2757
+ const logger = createLogger({
2758
+ level: getLogLevelFromEnv(),
2759
+ prefix: "cli"
2760
+ });
2761
+ logger.info("Starting React DevTools MCP Server...");
2762
+ const { bridge, start } = createServer({
2763
+ host: process.env.DEVTOOLS_HOST,
2764
+ port: process.env.DEVTOOLS_PORT ? Number(process.env.DEVTOOLS_PORT) : void 0,
2765
+ autoConnect: process.env.DEVTOOLS_AUTO_CONNECT !== "false",
2766
+ logger
2767
+ });
2768
+ bridge.on("connected", () => {
2769
+ logger.info("Connected to DevTools backend");
2770
+ });
2771
+ bridge.on("disconnected", ({ code, reason }) => {
2772
+ logger.info("Disconnected from DevTools backend", { code, reason });
2773
+ });
2774
+ bridge.on("reconnecting", ({ attempt, delay }) => {
2775
+ logger.info("Reconnecting to DevTools backend", { attempt, delay });
2776
+ });
2777
+ bridge.on("reconnectFailed", ({ attempts }) => {
2778
+ logger.error("Failed to reconnect after max attempts", { attempts });
2779
+ });
2780
+ bridge.on("renderer", (info) => {
2781
+ logger.info("Renderer attached", { id: info.id, version: info.rendererVersion });
2782
+ });
2783
+ bridge.on("parseError", ({ error }) => {
2784
+ logger.error("Protocol parse error", { error });
2785
+ });
2786
+ const shutdown = () => {
2787
+ logger.info("Shutting down...");
2788
+ bridge.disconnect();
2789
+ process.exit(0);
2790
+ };
2791
+ process.on("SIGINT", shutdown);
2792
+ process.on("SIGTERM", shutdown);
2793
+ try {
2794
+ await start();
2795
+ logger.info("Server started successfully");
2796
+ } catch (error) {
2797
+ const message = error instanceof Error ? error.message : "Unknown error";
2798
+ logger.error("Failed to start server", { error: message });
2799
+ process.exit(1);
2800
+ }
2801
+ }
2802
+ main().catch((error) => {
2803
+ console.error("Fatal error:", error);
2804
+ process.exit(1);
2805
+ });
2806
+ //# sourceMappingURL=cli.js.map