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