metro-mcp 0.7.2 → 0.8.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/bin/metro-mcp.js +197 -71
- package/dist/index.js +197 -71
- package/dist/plugins/devtools.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/bin/metro-mcp.js
CHANGED
|
@@ -1010,9 +1010,9 @@ var require_is_wsl = __commonJS((exports, module) => {
|
|
|
1010
1010
|
});
|
|
1011
1011
|
|
|
1012
1012
|
// node_modules/chrome-launcher/dist/utils.js
|
|
1013
|
-
import { join
|
|
1013
|
+
import { join } from "path";
|
|
1014
1014
|
import childProcess from "child_process";
|
|
1015
|
-
import { mkdirSync
|
|
1015
|
+
import { mkdirSync } from "fs";
|
|
1016
1016
|
function defaults(val, def) {
|
|
1017
1017
|
return typeof val === "undefined" ? def : val;
|
|
1018
1018
|
}
|
|
@@ -1076,8 +1076,8 @@ function makeUnixTmpDir() {
|
|
|
1076
1076
|
function makeWin32TmpDir() {
|
|
1077
1077
|
const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
|
|
1078
1078
|
const randomNumber = Math.floor(Math.random() * 90000000 + 1e7);
|
|
1079
|
-
const tmpdir =
|
|
1080
|
-
|
|
1079
|
+
const tmpdir = join(winTmpPath, "lighthouse." + randomNumber);
|
|
1080
|
+
mkdirSync(tmpdir, { recursive: true });
|
|
1081
1081
|
return tmpdir;
|
|
1082
1082
|
}
|
|
1083
1083
|
var import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
|
|
@@ -1133,7 +1133,7 @@ __export(exports_chrome_finder, {
|
|
|
1133
1133
|
});
|
|
1134
1134
|
import fs from "fs";
|
|
1135
1135
|
import path from "path";
|
|
1136
|
-
import { homedir
|
|
1136
|
+
import { homedir } from "os";
|
|
1137
1137
|
import { execSync, execFileSync } from "child_process";
|
|
1138
1138
|
function darwinFast() {
|
|
1139
1139
|
const priorityOptions = [
|
|
@@ -1164,7 +1164,7 @@ function darwin() {
|
|
|
1164
1164
|
}
|
|
1165
1165
|
});
|
|
1166
1166
|
});
|
|
1167
|
-
const home = import_escape_string_regexp.default(process.env.HOME ||
|
|
1167
|
+
const home = import_escape_string_regexp.default(process.env.HOME || homedir());
|
|
1168
1168
|
const priorities = [
|
|
1169
1169
|
{ regex: new RegExp(`^${home}/Applications/.*Chrome\\.app`), weight: 50 },
|
|
1170
1170
|
{ regex: new RegExp(`^${home}/Applications/.*Chrome Canary\\.app`), weight: 51 },
|
|
@@ -1198,7 +1198,7 @@ function linux() {
|
|
|
1198
1198
|
installations.push(customChromePath);
|
|
1199
1199
|
}
|
|
1200
1200
|
const desktopInstallationFolders = [
|
|
1201
|
-
path.join(
|
|
1201
|
+
path.join(homedir(), ".local/share/applications/"),
|
|
1202
1202
|
"/usr/share/applications/"
|
|
1203
1203
|
];
|
|
1204
1204
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -2479,8 +2479,8 @@ var require_mkdirp_native = __commonJS((exports, module) => {
|
|
|
2479
2479
|
// node_modules/mkdirp/lib/use-native.js
|
|
2480
2480
|
var require_use_native = __commonJS((exports, module) => {
|
|
2481
2481
|
var fs3 = __require("fs");
|
|
2482
|
-
var
|
|
2483
|
-
var versArr =
|
|
2482
|
+
var version = process.env.__TESTING_MKDIRP_NODE_VERSION__ || process.version;
|
|
2483
|
+
var versArr = version.replace(/^v/, "").split(".");
|
|
2484
2484
|
var hasNative = +versArr[0] > 10 || +versArr[0] === 10 && +versArr[1] >= 12;
|
|
2485
2485
|
var useNative = !hasNative ? () => false : (opts) => opts.mkdir === fs3.mkdir;
|
|
2486
2486
|
var useNativeSync = !hasNative ? () => false : (opts) => opts.mkdirSync === fs3.mkdirSync;
|
|
@@ -3317,7 +3317,7 @@ function mergeConfig(target, source) {
|
|
|
3317
3317
|
// src/server.ts
|
|
3318
3318
|
import { exec } from "child_process";
|
|
3319
3319
|
import { promisify } from "util";
|
|
3320
|
-
import
|
|
3320
|
+
import fs5 from "fs";
|
|
3321
3321
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3322
3322
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3323
3323
|
import { z as z23 } from "zod";
|
|
@@ -3326,6 +3326,7 @@ import { z as z23 } from "zod";
|
|
|
3326
3326
|
import WebSocket from "ws";
|
|
3327
3327
|
import http from "http";
|
|
3328
3328
|
import WebSocket2, { WebSocketServer } from "ws";
|
|
3329
|
+
import fs3 from "fs";
|
|
3329
3330
|
function createLogger2(name) {
|
|
3330
3331
|
const prefix = `[metro-bridge:${name}]`;
|
|
3331
3332
|
return {
|
|
@@ -3396,7 +3397,15 @@ class CDPSession {
|
|
|
3396
3397
|
}
|
|
3397
3398
|
return new Promise((resolve, reject) => {
|
|
3398
3399
|
try {
|
|
3399
|
-
|
|
3400
|
+
const wsOrigin = (() => {
|
|
3401
|
+
try {
|
|
3402
|
+
const u = new URL(url.replace(/^wss?:\/\//, (m) => m === "wss://" ? "https://" : "http://"));
|
|
3403
|
+
return `${u.protocol}//${u.host}`;
|
|
3404
|
+
} catch {
|
|
3405
|
+
return "http://localhost:8081";
|
|
3406
|
+
}
|
|
3407
|
+
})();
|
|
3408
|
+
this.ws = new WebSocket(url, { headers: { Origin: wsOrigin } });
|
|
3400
3409
|
const socketForThisConnection = this.ws;
|
|
3401
3410
|
this.ws.on("open", () => {
|
|
3402
3411
|
this._isConnected = true;
|
|
@@ -3406,6 +3415,7 @@ class CDPSession {
|
|
|
3406
3415
|
resolve();
|
|
3407
3416
|
});
|
|
3408
3417
|
this.ws.on("message", (data) => {
|
|
3418
|
+
this.lastPingAt = Date.now();
|
|
3409
3419
|
this.handleMessage(wsDataToString(data));
|
|
3410
3420
|
});
|
|
3411
3421
|
this.ws.on("close", (code, reason) => {
|
|
@@ -3598,6 +3608,9 @@ async function scanMetroPorts(host, specificPort) {
|
|
|
3598
3608
|
}));
|
|
3599
3609
|
return results;
|
|
3600
3610
|
}
|
|
3611
|
+
function supportsMultipleDebuggers(target) {
|
|
3612
|
+
return target.reactNative?.capabilities?.supportsMultipleDebuggers === true;
|
|
3613
|
+
}
|
|
3601
3614
|
async function checkMetroStatus(host, port) {
|
|
3602
3615
|
try {
|
|
3603
3616
|
const response = await fetch(`http://${host}:${port}/status`, {
|
|
@@ -3627,6 +3640,10 @@ class CDPMultiplexer {
|
|
|
3627
3640
|
constructor(cdpSession, options) {
|
|
3628
3641
|
this.cdpSession = cdpSession;
|
|
3629
3642
|
this.protectedDomains = new Set(options?.protectedDomains ?? []);
|
|
3643
|
+
const target = cdpSession.getTarget();
|
|
3644
|
+
if (target?.reactNative?.capabilities?.supportsMultipleDebuggers) {
|
|
3645
|
+
logger3.debug("CDPMultiplexer is not needed: target reports supportsMultipleDebuggers=true. " + "On RN 0.85+ Metro handles multiple concurrent connections natively.");
|
|
3646
|
+
}
|
|
3630
3647
|
cdpSession.messageInterceptor = (parsed, raw) => this.handleUpstreamMessage(parsed, raw);
|
|
3631
3648
|
cdpSession.on("reconnected", () => {
|
|
3632
3649
|
this.reEnableDomains();
|
|
@@ -3876,6 +3893,77 @@ class CDPMultiplexer {
|
|
|
3876
3893
|
}
|
|
3877
3894
|
}
|
|
3878
3895
|
var logger4 = createLogger2("devtools");
|
|
3896
|
+
var DEFAULT_STATE_FILE = "/tmp/metro-bridge-devtools.json";
|
|
3897
|
+
async function findBrowserPath() {
|
|
3898
|
+
try {
|
|
3899
|
+
const { Launcher: Launcher2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
3900
|
+
const path2 = Launcher2.getFirstInstallation();
|
|
3901
|
+
if (path2)
|
|
3902
|
+
return path2;
|
|
3903
|
+
} catch {}
|
|
3904
|
+
try {
|
|
3905
|
+
const { Launcher: EdgeLauncher } = await Promise.resolve().then(() => __toESM(require_dist2(), 1));
|
|
3906
|
+
const path2 = EdgeLauncher.getFirstInstallation();
|
|
3907
|
+
if (path2)
|
|
3908
|
+
return path2;
|
|
3909
|
+
} catch {}
|
|
3910
|
+
return null;
|
|
3911
|
+
}
|
|
3912
|
+
async function tryFocusExisting(frontendUrl, stateFile) {
|
|
3913
|
+
try {
|
|
3914
|
+
const state = JSON.parse(fs3.readFileSync(stateFile, "utf8"));
|
|
3915
|
+
if (!state.pid || !state.remoteDebuggingPort)
|
|
3916
|
+
return false;
|
|
3917
|
+
try {
|
|
3918
|
+
process.kill(state.pid, 0);
|
|
3919
|
+
} catch {
|
|
3920
|
+
return false;
|
|
3921
|
+
}
|
|
3922
|
+
const resp = await fetch(`http://localhost:${state.remoteDebuggingPort}/json`, {
|
|
3923
|
+
signal: AbortSignal.timeout(1000)
|
|
3924
|
+
});
|
|
3925
|
+
if (!resp.ok)
|
|
3926
|
+
return false;
|
|
3927
|
+
const targets = await resp.json();
|
|
3928
|
+
const target = targets.find((t) => t.url?.includes("rn_fusebox") || t.url === frontendUrl);
|
|
3929
|
+
if (!target?.id)
|
|
3930
|
+
return false;
|
|
3931
|
+
const activate = await fetch(`http://localhost:${state.remoteDebuggingPort}/json/activate/${target.id}`, { signal: AbortSignal.timeout(1000) });
|
|
3932
|
+
return activate.ok;
|
|
3933
|
+
} catch {
|
|
3934
|
+
return false;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
async function launchBrowser(frontendUrl, stateFile) {
|
|
3938
|
+
const { launch: launch2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
3939
|
+
const chrome2 = await launch2({
|
|
3940
|
+
chromeFlags: [`--app=${frontendUrl}`, "--window-size=1200,600"]
|
|
3941
|
+
});
|
|
3942
|
+
chrome2.process.unref();
|
|
3943
|
+
try {
|
|
3944
|
+
fs3.writeFileSync(stateFile, JSON.stringify({ pid: chrome2.pid, remoteDebuggingPort: chrome2.port }));
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
logger4.warn("Failed to write devtools state:", err);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
async function openDevTools(frontendUrl, options) {
|
|
3950
|
+
const stateFile = options?.stateFile ?? DEFAULT_STATE_FILE;
|
|
3951
|
+
const browserPath = await findBrowserPath();
|
|
3952
|
+
if (browserPath) {
|
|
3953
|
+
try {
|
|
3954
|
+
const focused = await tryFocusExisting(frontendUrl, stateFile);
|
|
3955
|
+
if (!focused) {
|
|
3956
|
+
await launchBrowser(frontendUrl, stateFile);
|
|
3957
|
+
}
|
|
3958
|
+
return { opened: true, url: frontendUrl };
|
|
3959
|
+
} catch (err) {
|
|
3960
|
+
logger4.debug("Failed to open DevTools:", err);
|
|
3961
|
+
}
|
|
3962
|
+
} else {
|
|
3963
|
+
logger4.debug("No Chrome/Edge installation found");
|
|
3964
|
+
}
|
|
3965
|
+
return { opened: false, url: frontendUrl };
|
|
3966
|
+
}
|
|
3879
3967
|
|
|
3880
3968
|
// src/metro/events.ts
|
|
3881
3969
|
import WebSocket3 from "ws";
|
|
@@ -4029,7 +4117,7 @@ function extractCDPExceptionMessage(details, fallback = "Evaluation failed") {
|
|
|
4029
4117
|
// package.json
|
|
4030
4118
|
var package_default = {
|
|
4031
4119
|
name: "metro-mcp",
|
|
4032
|
-
version: "0.
|
|
4120
|
+
version: "0.8.0",
|
|
4033
4121
|
description: "Plugin-based MCP server for React Native/Expo runtime debugging, inspection, and automation via Metro/CDP",
|
|
4034
4122
|
homepage: "https://metromcp.dev",
|
|
4035
4123
|
repository: {
|
|
@@ -4090,7 +4178,7 @@ var package_default = {
|
|
|
4090
4178
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
4091
4179
|
"chrome-launcher": "^1.2.1",
|
|
4092
4180
|
"chromium-edge-launcher": "^0.3.0",
|
|
4093
|
-
"metro-bridge": "^0.
|
|
4181
|
+
"metro-bridge": "^0.2.2",
|
|
4094
4182
|
ws: "^8.20.0",
|
|
4095
4183
|
zod: "^3.24.4"
|
|
4096
4184
|
},
|
|
@@ -5119,7 +5207,7 @@ var reduxPlugin = definePlugin({
|
|
|
5119
5207
|
path: z7.string().optional().describe('Dot-separated path to a state slice (e.g., "user.profile")'),
|
|
5120
5208
|
compact: z7.boolean().default(false).describe("Return compact format")
|
|
5121
5209
|
}),
|
|
5122
|
-
handler: async ({ path, compact: isCompact }) => {
|
|
5210
|
+
handler: async ({ path: path2, compact: isCompact }) => {
|
|
5123
5211
|
const getStateExpr = `
|
|
5124
5212
|
(function() {
|
|
5125
5213
|
// Client SDK
|
|
@@ -5144,14 +5232,14 @@ var reduxPlugin = definePlugin({
|
|
|
5144
5232
|
return "Redux store not found. Ensure Redux DevTools extension is enabled, or use the metro-mcp client SDK, or expose your store globally.";
|
|
5145
5233
|
}
|
|
5146
5234
|
let result = state;
|
|
5147
|
-
if (
|
|
5148
|
-
const parts =
|
|
5235
|
+
if (path2 && typeof state === "object" && state !== null) {
|
|
5236
|
+
const parts = path2.split(".");
|
|
5149
5237
|
let current = state;
|
|
5150
5238
|
for (const part of parts) {
|
|
5151
5239
|
if (current && typeof current === "object") {
|
|
5152
5240
|
current = current[part];
|
|
5153
5241
|
} else {
|
|
5154
|
-
return `Path "${
|
|
5242
|
+
return `Path "${path2}" not found in state.`;
|
|
5155
5243
|
}
|
|
5156
5244
|
}
|
|
5157
5245
|
result = current;
|
|
@@ -6928,8 +7016,8 @@ var commandsPlugin = definePlugin({
|
|
|
6928
7016
|
// src/plugins/test-recorder.ts
|
|
6929
7017
|
import { z as z16 } from "zod";
|
|
6930
7018
|
import { readdir, readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
|
|
6931
|
-
import { homedir } from "node:os";
|
|
6932
|
-
import { join } from "node:path";
|
|
7019
|
+
import { homedir as homedir2 } from "node:os";
|
|
7020
|
+
import { join as join2 } from "node:path";
|
|
6933
7021
|
var CURRENT_ROUTE_JS = `
|
|
6934
7022
|
(function() {
|
|
6935
7023
|
try {
|
|
@@ -7124,7 +7212,7 @@ var START_RECORDING_JS = `
|
|
|
7124
7212
|
return true;
|
|
7125
7213
|
})()
|
|
7126
7214
|
`;
|
|
7127
|
-
var RECORDINGS_DIR =
|
|
7215
|
+
var RECORDINGS_DIR = join2(homedir2(), ".metro-mcp", "recordings");
|
|
7128
7216
|
function appiumSelector(ev) {
|
|
7129
7217
|
if (ev.testID)
|
|
7130
7218
|
return `~${ev.testID}`;
|
|
@@ -7374,7 +7462,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
7374
7462
|
}
|
|
7375
7463
|
const safe = filename.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7376
7464
|
await mkdir(RECORDINGS_DIR, { recursive: true });
|
|
7377
|
-
const filePath =
|
|
7465
|
+
const filePath = join2(RECORDINGS_DIR, `${safe}.json`);
|
|
7378
7466
|
await writeFile(filePath, JSON.stringify({ savedAt: new Date().toISOString(), events }, null, 2), "utf8");
|
|
7379
7467
|
return { saved: filePath, eventCount: events.length };
|
|
7380
7468
|
}
|
|
@@ -7386,7 +7474,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
7386
7474
|
}),
|
|
7387
7475
|
handler: async ({ filename }) => {
|
|
7388
7476
|
const safe = filename.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7389
|
-
const filePath =
|
|
7477
|
+
const filePath = join2(RECORDINGS_DIR, `${safe}.json`);
|
|
7390
7478
|
let raw;
|
|
7391
7479
|
try {
|
|
7392
7480
|
raw = await readFile3(filePath, "utf8");
|
|
@@ -8120,13 +8208,13 @@ ${NOT_SETUP_MSG}`);
|
|
|
8120
8208
|
parameters: z17.object({
|
|
8121
8209
|
clear: z17.boolean().default(false).describe("Clear the render buffer after reading.")
|
|
8122
8210
|
}),
|
|
8123
|
-
handler: async ({ clear }) => {
|
|
8211
|
+
handler: async ({ clear: clear2 }) => {
|
|
8124
8212
|
try {
|
|
8125
|
-
const raw = await ctx.evalInApp(
|
|
8213
|
+
const raw = await ctx.evalInApp(clear2 ? READ_AND_CLEAR_EXPR : READ_RENDERS_EXPR);
|
|
8126
8214
|
if (!raw)
|
|
8127
8215
|
return NOT_SETUP_MSG;
|
|
8128
8216
|
if (raw.length === 0)
|
|
8129
|
-
return
|
|
8217
|
+
return clear2 ? "Render buffer cleared (was already empty)." : "No renders recorded yet.";
|
|
8130
8218
|
return [...raw].sort((a, b) => b.actualDuration - a.actualDuration).map((r) => ({
|
|
8131
8219
|
id: r.id,
|
|
8132
8220
|
phase: r.phase,
|
|
@@ -8680,13 +8768,13 @@ var automationPlugin = definePlugin({
|
|
|
8680
8768
|
});
|
|
8681
8769
|
|
|
8682
8770
|
// src/plugins/statusline.ts
|
|
8683
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
8684
|
-
import { homedir as
|
|
8685
|
-
import { join as
|
|
8771
|
+
import { writeFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
8772
|
+
import { homedir as homedir3 } from "node:os";
|
|
8773
|
+
import { join as join3 } from "node:path";
|
|
8686
8774
|
import { z as z19 } from "zod";
|
|
8687
8775
|
var STATUS_FILE = "/tmp/metro-mcp-status.json";
|
|
8688
|
-
var CLAUDE_DIR =
|
|
8689
|
-
var SCRIPT_PATH =
|
|
8776
|
+
var CLAUDE_DIR = join3(homedir3(), ".claude");
|
|
8777
|
+
var SCRIPT_PATH = join3(CLAUDE_DIR, "metro-mcp-statusline.sh");
|
|
8690
8778
|
var SCRIPT_CONTENT = `#!/bin/bash
|
|
8691
8779
|
# Metro MCP status line for Claude Code
|
|
8692
8780
|
# Shows CDP connection status of the Metro bundler.
|
|
@@ -8741,7 +8829,7 @@ var statuslinePlugin = definePlugin({
|
|
|
8741
8829
|
description: "Writes the Metro CDP connection status script to ~/.claude/metro-mcp-statusline.sh. " + "Does not modify settings.json — tell the user to add it to their status line themselves " + '(e.g. ask Claude: "/statusline add the script at ~/.claude/metro-mcp-statusline.sh"). ' + "Only works with Claude Code.",
|
|
8742
8830
|
parameters: z19.object({}),
|
|
8743
8831
|
handler: async () => {
|
|
8744
|
-
|
|
8832
|
+
mkdirSync2(CLAUDE_DIR, { recursive: true });
|
|
8745
8833
|
writeFileSync(SCRIPT_PATH, SCRIPT_CONTENT, { mode: 493 });
|
|
8746
8834
|
return [
|
|
8747
8835
|
`Metro MCP status script written to ${SCRIPT_PATH}`,
|
|
@@ -8937,11 +9025,14 @@ var inspectPointPlugin = definePlugin({
|
|
|
8937
9025
|
});
|
|
8938
9026
|
|
|
8939
9027
|
// src/plugins/devtools.ts
|
|
8940
|
-
import
|
|
9028
|
+
import fs4 from "fs";
|
|
8941
9029
|
import { z as z22 } from "zod";
|
|
8942
9030
|
var logger8 = createLogger("devtools");
|
|
8943
9031
|
var DEVTOOLS_STATE_FILE = "/tmp/metro-mcp-devtools.json";
|
|
8944
|
-
|
|
9032
|
+
function buildFrontendUrl(metroHost, metroPort, wsEndpoint) {
|
|
9033
|
+
return `http://${metroHost}:${metroPort}/debugger-frontend/rn_fusebox.html?ws=${wsEndpoint}&sources.hide_add_folder=true`;
|
|
9034
|
+
}
|
|
9035
|
+
async function findBrowserPath2() {
|
|
8945
9036
|
try {
|
|
8946
9037
|
const { Launcher: Launcher2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
8947
9038
|
const path2 = Launcher2.getFirstInstallation();
|
|
@@ -8956,9 +9047,9 @@ async function findBrowserPath() {
|
|
|
8956
9047
|
} catch {}
|
|
8957
9048
|
return null;
|
|
8958
9049
|
}
|
|
8959
|
-
async function
|
|
9050
|
+
async function tryFocusExisting2(frontendUrl) {
|
|
8960
9051
|
try {
|
|
8961
|
-
const state = JSON.parse(
|
|
9052
|
+
const state = JSON.parse(fs4.readFileSync(DEVTOOLS_STATE_FILE, "utf8"));
|
|
8962
9053
|
if (!state.pid || !state.remoteDebuggingPort)
|
|
8963
9054
|
return false;
|
|
8964
9055
|
try {
|
|
@@ -8988,7 +9079,7 @@ async function launchDevTools(frontendUrl) {
|
|
|
8988
9079
|
});
|
|
8989
9080
|
chrome2.process.unref();
|
|
8990
9081
|
try {
|
|
8991
|
-
|
|
9082
|
+
fs4.writeFileSync(DEVTOOLS_STATE_FILE, JSON.stringify({ pid: chrome2.pid, remoteDebuggingPort: chrome2.port }));
|
|
8992
9083
|
} catch (err) {
|
|
8993
9084
|
logger8.warn("Failed to write devtools state:", err);
|
|
8994
9085
|
}
|
|
@@ -8998,23 +9089,54 @@ var devtoolsPlugin = definePlugin({
|
|
|
8998
9089
|
description: "Open React Native DevTools via the CDP proxy",
|
|
8999
9090
|
async setup(ctx) {
|
|
9000
9091
|
ctx.registerTool("open_devtools", {
|
|
9001
|
-
description: "Open the React Native DevTools debugger panel in Chrome.
|
|
9092
|
+
description: "Open the React Native DevTools debugger panel in Chrome. On RN 0.85+ connects directly to Metro (no proxy needed). On older RN versions connects through the CDP proxy so both DevTools and the MCP can share the single Hermes connection.",
|
|
9002
9093
|
parameters: z22.object({
|
|
9003
9094
|
open: z22.boolean().default(true).describe("Attempt to open the browser automatically")
|
|
9004
9095
|
}),
|
|
9005
9096
|
handler: async ({ open }) => {
|
|
9097
|
+
const target = ctx.cdp.getTarget();
|
|
9098
|
+
if (!target) {
|
|
9099
|
+
return "Not connected to Metro. Start your React Native app and try again.";
|
|
9100
|
+
}
|
|
9101
|
+
if (supportsMultipleDebuggers(target)) {
|
|
9102
|
+
let frontendUrl2;
|
|
9103
|
+
if (target.devtoolsFrontendUrl) {
|
|
9104
|
+
frontendUrl2 = `http://${ctx.metro.host}:${ctx.metro.port}${target.devtoolsFrontendUrl}`;
|
|
9105
|
+
} else {
|
|
9106
|
+
let wsHost;
|
|
9107
|
+
try {
|
|
9108
|
+
wsHost = new URL(target.webSocketDebuggerUrl).host;
|
|
9109
|
+
} catch {
|
|
9110
|
+
return "Could not parse Metro WebSocket URL. Try reconnecting.";
|
|
9111
|
+
}
|
|
9112
|
+
frontendUrl2 = buildFrontendUrl(ctx.metro.host, ctx.metro.port, wsHost);
|
|
9113
|
+
}
|
|
9114
|
+
if (open) {
|
|
9115
|
+
try {
|
|
9116
|
+
const result = await openDevTools(frontendUrl2);
|
|
9117
|
+
return { opened: result.opened, url: frontendUrl2 };
|
|
9118
|
+
} catch (err) {
|
|
9119
|
+
logger8.debug("Failed to open DevTools:", err);
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
return {
|
|
9123
|
+
opened: false,
|
|
9124
|
+
url: frontendUrl2,
|
|
9125
|
+
instructions: "Open this URL in Chrome or Edge: " + frontendUrl2
|
|
9126
|
+
};
|
|
9127
|
+
}
|
|
9006
9128
|
const config = ctx.config;
|
|
9007
9129
|
const proxyConfig = config.proxy;
|
|
9008
9130
|
const proxyPort = proxyConfig?.port;
|
|
9009
9131
|
if (!proxyPort) {
|
|
9010
9132
|
return "CDP proxy is not running. Set proxy.enabled to true in your metro-mcp config.";
|
|
9011
9133
|
}
|
|
9012
|
-
const frontendUrl =
|
|
9134
|
+
const frontendUrl = buildFrontendUrl(ctx.metro.host, ctx.metro.port, `127.0.0.1:${proxyPort}`);
|
|
9013
9135
|
if (open) {
|
|
9014
|
-
const browserPath = await
|
|
9136
|
+
const browserPath = await findBrowserPath2();
|
|
9015
9137
|
if (browserPath) {
|
|
9016
9138
|
try {
|
|
9017
|
-
const focused = await
|
|
9139
|
+
const focused = await tryFocusExisting2(frontendUrl);
|
|
9018
9140
|
if (!focused) {
|
|
9019
9141
|
await launchDevTools(frontendUrl);
|
|
9020
9142
|
}
|
|
@@ -9248,7 +9370,7 @@ async function startServer(config) {
|
|
|
9248
9370
|
let isPrimaryInstance = false;
|
|
9249
9371
|
async function tryConnectViaProxy() {
|
|
9250
9372
|
try {
|
|
9251
|
-
const lockData = JSON.parse(
|
|
9373
|
+
const lockData = JSON.parse(fs5.readFileSync(PROXY_LOCK_FILE, "utf8"));
|
|
9252
9374
|
if (lockData.pid && lockData.port) {
|
|
9253
9375
|
try {
|
|
9254
9376
|
process.kill(lockData.pid, 0);
|
|
@@ -9282,7 +9404,7 @@ async function startServer(config) {
|
|
|
9282
9404
|
}
|
|
9283
9405
|
function writeProxyLock(proxyPort, metroPort) {
|
|
9284
9406
|
try {
|
|
9285
|
-
|
|
9407
|
+
fs5.writeFileSync(PROXY_LOCK_FILE, JSON.stringify({ pid: process.pid, port: proxyPort, metroPort }));
|
|
9286
9408
|
isPrimaryInstance = true;
|
|
9287
9409
|
logger9.info(`Wrote proxy lock (port ${proxyPort})`);
|
|
9288
9410
|
} catch (err) {
|
|
@@ -9293,9 +9415,9 @@ async function startServer(config) {
|
|
|
9293
9415
|
if (!isPrimaryInstance)
|
|
9294
9416
|
return;
|
|
9295
9417
|
try {
|
|
9296
|
-
const lockData = JSON.parse(
|
|
9418
|
+
const lockData = JSON.parse(fs5.readFileSync(PROXY_LOCK_FILE, "utf8"));
|
|
9297
9419
|
if (lockData.pid === process.pid) {
|
|
9298
|
-
|
|
9420
|
+
fs5.unlinkSync(PROXY_LOCK_FILE);
|
|
9299
9421
|
}
|
|
9300
9422
|
} catch {}
|
|
9301
9423
|
}
|
|
@@ -9331,9 +9453,31 @@ async function startServer(config) {
|
|
|
9331
9453
|
eventsClient.connect(server.host, server.port);
|
|
9332
9454
|
activeDeviceKey = `${server.port}-${target.id}`;
|
|
9333
9455
|
activeDeviceName = target.title || target.deviceName || target.id;
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9456
|
+
if (supportsMultipleDebuggers(target)) {
|
|
9457
|
+
logger9.info("Target supports multiple debuggers (RN 0.85+) — skipping CDP proxy");
|
|
9458
|
+
} else {
|
|
9459
|
+
if (!cdpMultiplexer && config.proxy?.enabled !== false) {
|
|
9460
|
+
const mux = new CDPMultiplexer(cdpSession, { protectedDomains: ["Runtime", "Network"] });
|
|
9461
|
+
try {
|
|
9462
|
+
const startedPort = await mux.start(preferredProxyPort);
|
|
9463
|
+
const devtoolsUrl = mux.getDevToolsUrl();
|
|
9464
|
+
logger9.info(`CDP proxy started on port ${startedPort}`);
|
|
9465
|
+
if (devtoolsUrl)
|
|
9466
|
+
logger9.info(`Chrome DevTools URL: ${devtoolsUrl}`);
|
|
9467
|
+
config.proxy = {
|
|
9468
|
+
...config.proxy,
|
|
9469
|
+
port: startedPort,
|
|
9470
|
+
url: devtoolsUrl
|
|
9471
|
+
};
|
|
9472
|
+
cdpMultiplexer = mux;
|
|
9473
|
+
} catch (err) {
|
|
9474
|
+
logger9.warn("Could not start CDP proxy:", err);
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
const proxyConfig = config.proxy;
|
|
9478
|
+
if (proxyConfig?.port) {
|
|
9479
|
+
writeProxyLock(proxyConfig.port, server.port);
|
|
9480
|
+
}
|
|
9337
9481
|
}
|
|
9338
9482
|
return true;
|
|
9339
9483
|
} catch (err) {
|
|
@@ -9364,31 +9508,13 @@ async function startServer(config) {
|
|
|
9364
9508
|
}, delay2);
|
|
9365
9509
|
}
|
|
9366
9510
|
let cdpMultiplexer = null;
|
|
9367
|
-
|
|
9368
|
-
|
|
9511
|
+
let preferredProxyPort = config.proxy?.port ?? 0;
|
|
9512
|
+
if (preferredProxyPort === 0) {
|
|
9369
9513
|
try {
|
|
9370
|
-
|
|
9371
|
-
if (
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
if (stale.port)
|
|
9375
|
-
preferredProxyPort = stale.port;
|
|
9376
|
-
} catch {}
|
|
9377
|
-
}
|
|
9378
|
-
const proxyPort = await cdpMultiplexer.start(preferredProxyPort);
|
|
9379
|
-
const devtoolsUrl = cdpMultiplexer.getDevToolsUrl();
|
|
9380
|
-
logger9.info(`CDP proxy started on port ${proxyPort}`);
|
|
9381
|
-
if (devtoolsUrl) {
|
|
9382
|
-
logger9.info(`Chrome DevTools URL: ${devtoolsUrl}`);
|
|
9383
|
-
}
|
|
9384
|
-
config.proxy = {
|
|
9385
|
-
...config.proxy,
|
|
9386
|
-
port: proxyPort,
|
|
9387
|
-
url: devtoolsUrl
|
|
9388
|
-
};
|
|
9389
|
-
} catch (err) {
|
|
9390
|
-
logger9.warn("Could not start CDP proxy:", err);
|
|
9391
|
-
}
|
|
9514
|
+
const stale = JSON.parse(fs5.readFileSync(PROXY_LOCK_FILE, "utf8"));
|
|
9515
|
+
if (stale.port)
|
|
9516
|
+
preferredProxyPort = stale.port;
|
|
9517
|
+
} catch {}
|
|
9392
9518
|
}
|
|
9393
9519
|
const transport = new StdioServerTransport;
|
|
9394
9520
|
await mcpServer.connect(transport);
|
package/dist/index.js
CHANGED
|
@@ -1010,9 +1010,9 @@ var require_is_wsl = __commonJS((exports, module) => {
|
|
|
1010
1010
|
});
|
|
1011
1011
|
|
|
1012
1012
|
// node_modules/chrome-launcher/dist/utils.js
|
|
1013
|
-
import { join
|
|
1013
|
+
import { join } from "path";
|
|
1014
1014
|
import childProcess from "child_process";
|
|
1015
|
-
import { mkdirSync
|
|
1015
|
+
import { mkdirSync } from "fs";
|
|
1016
1016
|
function defaults(val, def) {
|
|
1017
1017
|
return typeof val === "undefined" ? def : val;
|
|
1018
1018
|
}
|
|
@@ -1076,8 +1076,8 @@ function makeUnixTmpDir() {
|
|
|
1076
1076
|
function makeWin32TmpDir() {
|
|
1077
1077
|
const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
|
|
1078
1078
|
const randomNumber = Math.floor(Math.random() * 90000000 + 1e7);
|
|
1079
|
-
const tmpdir =
|
|
1080
|
-
|
|
1079
|
+
const tmpdir = join(winTmpPath, "lighthouse." + randomNumber);
|
|
1080
|
+
mkdirSync(tmpdir, { recursive: true });
|
|
1081
1081
|
return tmpdir;
|
|
1082
1082
|
}
|
|
1083
1083
|
var import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
|
|
@@ -1133,7 +1133,7 @@ __export(exports_chrome_finder, {
|
|
|
1133
1133
|
});
|
|
1134
1134
|
import fs from "fs";
|
|
1135
1135
|
import path from "path";
|
|
1136
|
-
import { homedir
|
|
1136
|
+
import { homedir } from "os";
|
|
1137
1137
|
import { execSync, execFileSync } from "child_process";
|
|
1138
1138
|
function darwinFast() {
|
|
1139
1139
|
const priorityOptions = [
|
|
@@ -1164,7 +1164,7 @@ function darwin() {
|
|
|
1164
1164
|
}
|
|
1165
1165
|
});
|
|
1166
1166
|
});
|
|
1167
|
-
const home = import_escape_string_regexp.default(process.env.HOME ||
|
|
1167
|
+
const home = import_escape_string_regexp.default(process.env.HOME || homedir());
|
|
1168
1168
|
const priorities = [
|
|
1169
1169
|
{ regex: new RegExp(`^${home}/Applications/.*Chrome\\.app`), weight: 50 },
|
|
1170
1170
|
{ regex: new RegExp(`^${home}/Applications/.*Chrome Canary\\.app`), weight: 51 },
|
|
@@ -1198,7 +1198,7 @@ function linux() {
|
|
|
1198
1198
|
installations.push(customChromePath);
|
|
1199
1199
|
}
|
|
1200
1200
|
const desktopInstallationFolders = [
|
|
1201
|
-
path.join(
|
|
1201
|
+
path.join(homedir(), ".local/share/applications/"),
|
|
1202
1202
|
"/usr/share/applications/"
|
|
1203
1203
|
];
|
|
1204
1204
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -2479,8 +2479,8 @@ var require_mkdirp_native = __commonJS((exports, module) => {
|
|
|
2479
2479
|
// node_modules/mkdirp/lib/use-native.js
|
|
2480
2480
|
var require_use_native = __commonJS((exports, module) => {
|
|
2481
2481
|
var fs3 = __require("fs");
|
|
2482
|
-
var
|
|
2483
|
-
var versArr =
|
|
2482
|
+
var version = process.env.__TESTING_MKDIRP_NODE_VERSION__ || process.version;
|
|
2483
|
+
var versArr = version.replace(/^v/, "").split(".");
|
|
2484
2484
|
var hasNative = +versArr[0] > 10 || +versArr[0] === 10 && +versArr[1] >= 12;
|
|
2485
2485
|
var useNative = !hasNative ? () => false : (opts) => opts.mkdir === fs3.mkdir;
|
|
2486
2486
|
var useNativeSync = !hasNative ? () => false : (opts) => opts.mkdirSync === fs3.mkdirSync;
|
|
@@ -3325,7 +3325,7 @@ function mergeConfig(target, source) {
|
|
|
3325
3325
|
// src/server.ts
|
|
3326
3326
|
import { exec } from "child_process";
|
|
3327
3327
|
import { promisify } from "util";
|
|
3328
|
-
import
|
|
3328
|
+
import fs5 from "fs";
|
|
3329
3329
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3330
3330
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3331
3331
|
import { z as z23 } from "zod";
|
|
@@ -3334,6 +3334,7 @@ import { z as z23 } from "zod";
|
|
|
3334
3334
|
import WebSocket from "ws";
|
|
3335
3335
|
import http from "http";
|
|
3336
3336
|
import WebSocket2, { WebSocketServer } from "ws";
|
|
3337
|
+
import fs3 from "fs";
|
|
3337
3338
|
function createLogger2(name) {
|
|
3338
3339
|
const prefix = `[metro-bridge:${name}]`;
|
|
3339
3340
|
return {
|
|
@@ -3404,7 +3405,15 @@ class CDPSession {
|
|
|
3404
3405
|
}
|
|
3405
3406
|
return new Promise((resolve, reject) => {
|
|
3406
3407
|
try {
|
|
3407
|
-
|
|
3408
|
+
const wsOrigin = (() => {
|
|
3409
|
+
try {
|
|
3410
|
+
const u = new URL(url.replace(/^wss?:\/\//, (m) => m === "wss://" ? "https://" : "http://"));
|
|
3411
|
+
return `${u.protocol}//${u.host}`;
|
|
3412
|
+
} catch {
|
|
3413
|
+
return "http://localhost:8081";
|
|
3414
|
+
}
|
|
3415
|
+
})();
|
|
3416
|
+
this.ws = new WebSocket(url, { headers: { Origin: wsOrigin } });
|
|
3408
3417
|
const socketForThisConnection = this.ws;
|
|
3409
3418
|
this.ws.on("open", () => {
|
|
3410
3419
|
this._isConnected = true;
|
|
@@ -3414,6 +3423,7 @@ class CDPSession {
|
|
|
3414
3423
|
resolve();
|
|
3415
3424
|
});
|
|
3416
3425
|
this.ws.on("message", (data) => {
|
|
3426
|
+
this.lastPingAt = Date.now();
|
|
3417
3427
|
this.handleMessage(wsDataToString(data));
|
|
3418
3428
|
});
|
|
3419
3429
|
this.ws.on("close", (code, reason) => {
|
|
@@ -3606,6 +3616,9 @@ async function scanMetroPorts(host, specificPort) {
|
|
|
3606
3616
|
}));
|
|
3607
3617
|
return results;
|
|
3608
3618
|
}
|
|
3619
|
+
function supportsMultipleDebuggers(target) {
|
|
3620
|
+
return target.reactNative?.capabilities?.supportsMultipleDebuggers === true;
|
|
3621
|
+
}
|
|
3609
3622
|
async function checkMetroStatus(host, port) {
|
|
3610
3623
|
try {
|
|
3611
3624
|
const response = await fetch(`http://${host}:${port}/status`, {
|
|
@@ -3635,6 +3648,10 @@ class CDPMultiplexer {
|
|
|
3635
3648
|
constructor(cdpSession, options) {
|
|
3636
3649
|
this.cdpSession = cdpSession;
|
|
3637
3650
|
this.protectedDomains = new Set(options?.protectedDomains ?? []);
|
|
3651
|
+
const target = cdpSession.getTarget();
|
|
3652
|
+
if (target?.reactNative?.capabilities?.supportsMultipleDebuggers) {
|
|
3653
|
+
logger3.debug("CDPMultiplexer is not needed: target reports supportsMultipleDebuggers=true. " + "On RN 0.85+ Metro handles multiple concurrent connections natively.");
|
|
3654
|
+
}
|
|
3638
3655
|
cdpSession.messageInterceptor = (parsed, raw) => this.handleUpstreamMessage(parsed, raw);
|
|
3639
3656
|
cdpSession.on("reconnected", () => {
|
|
3640
3657
|
this.reEnableDomains();
|
|
@@ -3884,6 +3901,77 @@ class CDPMultiplexer {
|
|
|
3884
3901
|
}
|
|
3885
3902
|
}
|
|
3886
3903
|
var logger4 = createLogger2("devtools");
|
|
3904
|
+
var DEFAULT_STATE_FILE = "/tmp/metro-bridge-devtools.json";
|
|
3905
|
+
async function findBrowserPath() {
|
|
3906
|
+
try {
|
|
3907
|
+
const { Launcher: Launcher2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
3908
|
+
const path2 = Launcher2.getFirstInstallation();
|
|
3909
|
+
if (path2)
|
|
3910
|
+
return path2;
|
|
3911
|
+
} catch {}
|
|
3912
|
+
try {
|
|
3913
|
+
const { Launcher: EdgeLauncher } = await Promise.resolve().then(() => __toESM(require_dist2(), 1));
|
|
3914
|
+
const path2 = EdgeLauncher.getFirstInstallation();
|
|
3915
|
+
if (path2)
|
|
3916
|
+
return path2;
|
|
3917
|
+
} catch {}
|
|
3918
|
+
return null;
|
|
3919
|
+
}
|
|
3920
|
+
async function tryFocusExisting(frontendUrl, stateFile) {
|
|
3921
|
+
try {
|
|
3922
|
+
const state = JSON.parse(fs3.readFileSync(stateFile, "utf8"));
|
|
3923
|
+
if (!state.pid || !state.remoteDebuggingPort)
|
|
3924
|
+
return false;
|
|
3925
|
+
try {
|
|
3926
|
+
process.kill(state.pid, 0);
|
|
3927
|
+
} catch {
|
|
3928
|
+
return false;
|
|
3929
|
+
}
|
|
3930
|
+
const resp = await fetch(`http://localhost:${state.remoteDebuggingPort}/json`, {
|
|
3931
|
+
signal: AbortSignal.timeout(1000)
|
|
3932
|
+
});
|
|
3933
|
+
if (!resp.ok)
|
|
3934
|
+
return false;
|
|
3935
|
+
const targets = await resp.json();
|
|
3936
|
+
const target = targets.find((t) => t.url?.includes("rn_fusebox") || t.url === frontendUrl);
|
|
3937
|
+
if (!target?.id)
|
|
3938
|
+
return false;
|
|
3939
|
+
const activate = await fetch(`http://localhost:${state.remoteDebuggingPort}/json/activate/${target.id}`, { signal: AbortSignal.timeout(1000) });
|
|
3940
|
+
return activate.ok;
|
|
3941
|
+
} catch {
|
|
3942
|
+
return false;
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
async function launchBrowser(frontendUrl, stateFile) {
|
|
3946
|
+
const { launch: launch2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
3947
|
+
const chrome2 = await launch2({
|
|
3948
|
+
chromeFlags: [`--app=${frontendUrl}`, "--window-size=1200,600"]
|
|
3949
|
+
});
|
|
3950
|
+
chrome2.process.unref();
|
|
3951
|
+
try {
|
|
3952
|
+
fs3.writeFileSync(stateFile, JSON.stringify({ pid: chrome2.pid, remoteDebuggingPort: chrome2.port }));
|
|
3953
|
+
} catch (err) {
|
|
3954
|
+
logger4.warn("Failed to write devtools state:", err);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
async function openDevTools(frontendUrl, options) {
|
|
3958
|
+
const stateFile = options?.stateFile ?? DEFAULT_STATE_FILE;
|
|
3959
|
+
const browserPath = await findBrowserPath();
|
|
3960
|
+
if (browserPath) {
|
|
3961
|
+
try {
|
|
3962
|
+
const focused = await tryFocusExisting(frontendUrl, stateFile);
|
|
3963
|
+
if (!focused) {
|
|
3964
|
+
await launchBrowser(frontendUrl, stateFile);
|
|
3965
|
+
}
|
|
3966
|
+
return { opened: true, url: frontendUrl };
|
|
3967
|
+
} catch (err) {
|
|
3968
|
+
logger4.debug("Failed to open DevTools:", err);
|
|
3969
|
+
}
|
|
3970
|
+
} else {
|
|
3971
|
+
logger4.debug("No Chrome/Edge installation found");
|
|
3972
|
+
}
|
|
3973
|
+
return { opened: false, url: frontendUrl };
|
|
3974
|
+
}
|
|
3887
3975
|
|
|
3888
3976
|
// src/metro/events.ts
|
|
3889
3977
|
import WebSocket3 from "ws";
|
|
@@ -4037,7 +4125,7 @@ function extractCDPExceptionMessage(details, fallback = "Evaluation failed") {
|
|
|
4037
4125
|
// package.json
|
|
4038
4126
|
var package_default = {
|
|
4039
4127
|
name: "metro-mcp",
|
|
4040
|
-
version: "0.
|
|
4128
|
+
version: "0.8.0",
|
|
4041
4129
|
description: "Plugin-based MCP server for React Native/Expo runtime debugging, inspection, and automation via Metro/CDP",
|
|
4042
4130
|
homepage: "https://metromcp.dev",
|
|
4043
4131
|
repository: {
|
|
@@ -4098,7 +4186,7 @@ var package_default = {
|
|
|
4098
4186
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
4099
4187
|
"chrome-launcher": "^1.2.1",
|
|
4100
4188
|
"chromium-edge-launcher": "^0.3.0",
|
|
4101
|
-
"metro-bridge": "^0.
|
|
4189
|
+
"metro-bridge": "^0.2.2",
|
|
4102
4190
|
ws: "^8.20.0",
|
|
4103
4191
|
zod: "^3.24.4"
|
|
4104
4192
|
},
|
|
@@ -5122,7 +5210,7 @@ var reduxPlugin = definePlugin({
|
|
|
5122
5210
|
path: z7.string().optional().describe('Dot-separated path to a state slice (e.g., "user.profile")'),
|
|
5123
5211
|
compact: z7.boolean().default(false).describe("Return compact format")
|
|
5124
5212
|
}),
|
|
5125
|
-
handler: async ({ path, compact: isCompact }) => {
|
|
5213
|
+
handler: async ({ path: path2, compact: isCompact }) => {
|
|
5126
5214
|
const getStateExpr = `
|
|
5127
5215
|
(function() {
|
|
5128
5216
|
// Client SDK
|
|
@@ -5147,14 +5235,14 @@ var reduxPlugin = definePlugin({
|
|
|
5147
5235
|
return "Redux store not found. Ensure Redux DevTools extension is enabled, or use the metro-mcp client SDK, or expose your store globally.";
|
|
5148
5236
|
}
|
|
5149
5237
|
let result = state;
|
|
5150
|
-
if (
|
|
5151
|
-
const parts =
|
|
5238
|
+
if (path2 && typeof state === "object" && state !== null) {
|
|
5239
|
+
const parts = path2.split(".");
|
|
5152
5240
|
let current = state;
|
|
5153
5241
|
for (const part of parts) {
|
|
5154
5242
|
if (current && typeof current === "object") {
|
|
5155
5243
|
current = current[part];
|
|
5156
5244
|
} else {
|
|
5157
|
-
return `Path "${
|
|
5245
|
+
return `Path "${path2}" not found in state.`;
|
|
5158
5246
|
}
|
|
5159
5247
|
}
|
|
5160
5248
|
result = current;
|
|
@@ -6931,8 +7019,8 @@ var commandsPlugin = definePlugin({
|
|
|
6931
7019
|
// src/plugins/test-recorder.ts
|
|
6932
7020
|
import { z as z16 } from "zod";
|
|
6933
7021
|
import { readdir, readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
|
|
6934
|
-
import { homedir } from "node:os";
|
|
6935
|
-
import { join } from "node:path";
|
|
7022
|
+
import { homedir as homedir2 } from "node:os";
|
|
7023
|
+
import { join as join2 } from "node:path";
|
|
6936
7024
|
var CURRENT_ROUTE_JS = `
|
|
6937
7025
|
(function() {
|
|
6938
7026
|
try {
|
|
@@ -7127,7 +7215,7 @@ var START_RECORDING_JS = `
|
|
|
7127
7215
|
return true;
|
|
7128
7216
|
})()
|
|
7129
7217
|
`;
|
|
7130
|
-
var RECORDINGS_DIR =
|
|
7218
|
+
var RECORDINGS_DIR = join2(homedir2(), ".metro-mcp", "recordings");
|
|
7131
7219
|
function appiumSelector(ev) {
|
|
7132
7220
|
if (ev.testID)
|
|
7133
7221
|
return `~${ev.testID}`;
|
|
@@ -7377,7 +7465,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
7377
7465
|
}
|
|
7378
7466
|
const safe = filename.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7379
7467
|
await mkdir(RECORDINGS_DIR, { recursive: true });
|
|
7380
|
-
const filePath =
|
|
7468
|
+
const filePath = join2(RECORDINGS_DIR, `${safe}.json`);
|
|
7381
7469
|
await writeFile(filePath, JSON.stringify({ savedAt: new Date().toISOString(), events }, null, 2), "utf8");
|
|
7382
7470
|
return { saved: filePath, eventCount: events.length };
|
|
7383
7471
|
}
|
|
@@ -7389,7 +7477,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
7389
7477
|
}),
|
|
7390
7478
|
handler: async ({ filename }) => {
|
|
7391
7479
|
const safe = filename.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7392
|
-
const filePath =
|
|
7480
|
+
const filePath = join2(RECORDINGS_DIR, `${safe}.json`);
|
|
7393
7481
|
let raw;
|
|
7394
7482
|
try {
|
|
7395
7483
|
raw = await readFile3(filePath, "utf8");
|
|
@@ -8123,13 +8211,13 @@ ${NOT_SETUP_MSG}`);
|
|
|
8123
8211
|
parameters: z17.object({
|
|
8124
8212
|
clear: z17.boolean().default(false).describe("Clear the render buffer after reading.")
|
|
8125
8213
|
}),
|
|
8126
|
-
handler: async ({ clear }) => {
|
|
8214
|
+
handler: async ({ clear: clear2 }) => {
|
|
8127
8215
|
try {
|
|
8128
|
-
const raw = await ctx.evalInApp(
|
|
8216
|
+
const raw = await ctx.evalInApp(clear2 ? READ_AND_CLEAR_EXPR : READ_RENDERS_EXPR);
|
|
8129
8217
|
if (!raw)
|
|
8130
8218
|
return NOT_SETUP_MSG;
|
|
8131
8219
|
if (raw.length === 0)
|
|
8132
|
-
return
|
|
8220
|
+
return clear2 ? "Render buffer cleared (was already empty)." : "No renders recorded yet.";
|
|
8133
8221
|
return [...raw].sort((a, b) => b.actualDuration - a.actualDuration).map((r) => ({
|
|
8134
8222
|
id: r.id,
|
|
8135
8223
|
phase: r.phase,
|
|
@@ -8683,13 +8771,13 @@ var automationPlugin = definePlugin({
|
|
|
8683
8771
|
});
|
|
8684
8772
|
|
|
8685
8773
|
// src/plugins/statusline.ts
|
|
8686
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
8687
|
-
import { homedir as
|
|
8688
|
-
import { join as
|
|
8774
|
+
import { writeFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
8775
|
+
import { homedir as homedir3 } from "node:os";
|
|
8776
|
+
import { join as join3 } from "node:path";
|
|
8689
8777
|
import { z as z19 } from "zod";
|
|
8690
8778
|
var STATUS_FILE = "/tmp/metro-mcp-status.json";
|
|
8691
|
-
var CLAUDE_DIR =
|
|
8692
|
-
var SCRIPT_PATH =
|
|
8779
|
+
var CLAUDE_DIR = join3(homedir3(), ".claude");
|
|
8780
|
+
var SCRIPT_PATH = join3(CLAUDE_DIR, "metro-mcp-statusline.sh");
|
|
8693
8781
|
var SCRIPT_CONTENT = `#!/bin/bash
|
|
8694
8782
|
# Metro MCP status line for Claude Code
|
|
8695
8783
|
# Shows CDP connection status of the Metro bundler.
|
|
@@ -8744,7 +8832,7 @@ var statuslinePlugin = definePlugin({
|
|
|
8744
8832
|
description: "Writes the Metro CDP connection status script to ~/.claude/metro-mcp-statusline.sh. " + "Does not modify settings.json — tell the user to add it to their status line themselves " + '(e.g. ask Claude: "/statusline add the script at ~/.claude/metro-mcp-statusline.sh"). ' + "Only works with Claude Code.",
|
|
8745
8833
|
parameters: z19.object({}),
|
|
8746
8834
|
handler: async () => {
|
|
8747
|
-
|
|
8835
|
+
mkdirSync2(CLAUDE_DIR, { recursive: true });
|
|
8748
8836
|
writeFileSync(SCRIPT_PATH, SCRIPT_CONTENT, { mode: 493 });
|
|
8749
8837
|
return [
|
|
8750
8838
|
`Metro MCP status script written to ${SCRIPT_PATH}`,
|
|
@@ -8940,11 +9028,14 @@ var inspectPointPlugin = definePlugin({
|
|
|
8940
9028
|
});
|
|
8941
9029
|
|
|
8942
9030
|
// src/plugins/devtools.ts
|
|
8943
|
-
import
|
|
9031
|
+
import fs4 from "fs";
|
|
8944
9032
|
import { z as z22 } from "zod";
|
|
8945
9033
|
var logger8 = createLogger("devtools");
|
|
8946
9034
|
var DEVTOOLS_STATE_FILE = "/tmp/metro-mcp-devtools.json";
|
|
8947
|
-
|
|
9035
|
+
function buildFrontendUrl(metroHost, metroPort, wsEndpoint) {
|
|
9036
|
+
return `http://${metroHost}:${metroPort}/debugger-frontend/rn_fusebox.html?ws=${wsEndpoint}&sources.hide_add_folder=true`;
|
|
9037
|
+
}
|
|
9038
|
+
async function findBrowserPath2() {
|
|
8948
9039
|
try {
|
|
8949
9040
|
const { Launcher: Launcher2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
8950
9041
|
const path2 = Launcher2.getFirstInstallation();
|
|
@@ -8959,9 +9050,9 @@ async function findBrowserPath() {
|
|
|
8959
9050
|
} catch {}
|
|
8960
9051
|
return null;
|
|
8961
9052
|
}
|
|
8962
|
-
async function
|
|
9053
|
+
async function tryFocusExisting2(frontendUrl) {
|
|
8963
9054
|
try {
|
|
8964
|
-
const state = JSON.parse(
|
|
9055
|
+
const state = JSON.parse(fs4.readFileSync(DEVTOOLS_STATE_FILE, "utf8"));
|
|
8965
9056
|
if (!state.pid || !state.remoteDebuggingPort)
|
|
8966
9057
|
return false;
|
|
8967
9058
|
try {
|
|
@@ -8991,7 +9082,7 @@ async function launchDevTools(frontendUrl) {
|
|
|
8991
9082
|
});
|
|
8992
9083
|
chrome2.process.unref();
|
|
8993
9084
|
try {
|
|
8994
|
-
|
|
9085
|
+
fs4.writeFileSync(DEVTOOLS_STATE_FILE, JSON.stringify({ pid: chrome2.pid, remoteDebuggingPort: chrome2.port }));
|
|
8995
9086
|
} catch (err) {
|
|
8996
9087
|
logger8.warn("Failed to write devtools state:", err);
|
|
8997
9088
|
}
|
|
@@ -9001,23 +9092,54 @@ var devtoolsPlugin = definePlugin({
|
|
|
9001
9092
|
description: "Open React Native DevTools via the CDP proxy",
|
|
9002
9093
|
async setup(ctx) {
|
|
9003
9094
|
ctx.registerTool("open_devtools", {
|
|
9004
|
-
description: "Open the React Native DevTools debugger panel in Chrome.
|
|
9095
|
+
description: "Open the React Native DevTools debugger panel in Chrome. On RN 0.85+ connects directly to Metro (no proxy needed). On older RN versions connects through the CDP proxy so both DevTools and the MCP can share the single Hermes connection.",
|
|
9005
9096
|
parameters: z22.object({
|
|
9006
9097
|
open: z22.boolean().default(true).describe("Attempt to open the browser automatically")
|
|
9007
9098
|
}),
|
|
9008
9099
|
handler: async ({ open }) => {
|
|
9100
|
+
const target = ctx.cdp.getTarget();
|
|
9101
|
+
if (!target) {
|
|
9102
|
+
return "Not connected to Metro. Start your React Native app and try again.";
|
|
9103
|
+
}
|
|
9104
|
+
if (supportsMultipleDebuggers(target)) {
|
|
9105
|
+
let frontendUrl2;
|
|
9106
|
+
if (target.devtoolsFrontendUrl) {
|
|
9107
|
+
frontendUrl2 = `http://${ctx.metro.host}:${ctx.metro.port}${target.devtoolsFrontendUrl}`;
|
|
9108
|
+
} else {
|
|
9109
|
+
let wsHost;
|
|
9110
|
+
try {
|
|
9111
|
+
wsHost = new URL(target.webSocketDebuggerUrl).host;
|
|
9112
|
+
} catch {
|
|
9113
|
+
return "Could not parse Metro WebSocket URL. Try reconnecting.";
|
|
9114
|
+
}
|
|
9115
|
+
frontendUrl2 = buildFrontendUrl(ctx.metro.host, ctx.metro.port, wsHost);
|
|
9116
|
+
}
|
|
9117
|
+
if (open) {
|
|
9118
|
+
try {
|
|
9119
|
+
const result = await openDevTools(frontendUrl2);
|
|
9120
|
+
return { opened: result.opened, url: frontendUrl2 };
|
|
9121
|
+
} catch (err) {
|
|
9122
|
+
logger8.debug("Failed to open DevTools:", err);
|
|
9123
|
+
}
|
|
9124
|
+
}
|
|
9125
|
+
return {
|
|
9126
|
+
opened: false,
|
|
9127
|
+
url: frontendUrl2,
|
|
9128
|
+
instructions: "Open this URL in Chrome or Edge: " + frontendUrl2
|
|
9129
|
+
};
|
|
9130
|
+
}
|
|
9009
9131
|
const config = ctx.config;
|
|
9010
9132
|
const proxyConfig = config.proxy;
|
|
9011
9133
|
const proxyPort = proxyConfig?.port;
|
|
9012
9134
|
if (!proxyPort) {
|
|
9013
9135
|
return "CDP proxy is not running. Set proxy.enabled to true in your metro-mcp config.";
|
|
9014
9136
|
}
|
|
9015
|
-
const frontendUrl =
|
|
9137
|
+
const frontendUrl = buildFrontendUrl(ctx.metro.host, ctx.metro.port, `127.0.0.1:${proxyPort}`);
|
|
9016
9138
|
if (open) {
|
|
9017
|
-
const browserPath = await
|
|
9139
|
+
const browserPath = await findBrowserPath2();
|
|
9018
9140
|
if (browserPath) {
|
|
9019
9141
|
try {
|
|
9020
|
-
const focused = await
|
|
9142
|
+
const focused = await tryFocusExisting2(frontendUrl);
|
|
9021
9143
|
if (!focused) {
|
|
9022
9144
|
await launchDevTools(frontendUrl);
|
|
9023
9145
|
}
|
|
@@ -9251,7 +9373,7 @@ async function startServer(config) {
|
|
|
9251
9373
|
let isPrimaryInstance = false;
|
|
9252
9374
|
async function tryConnectViaProxy() {
|
|
9253
9375
|
try {
|
|
9254
|
-
const lockData = JSON.parse(
|
|
9376
|
+
const lockData = JSON.parse(fs5.readFileSync(PROXY_LOCK_FILE, "utf8"));
|
|
9255
9377
|
if (lockData.pid && lockData.port) {
|
|
9256
9378
|
try {
|
|
9257
9379
|
process.kill(lockData.pid, 0);
|
|
@@ -9285,7 +9407,7 @@ async function startServer(config) {
|
|
|
9285
9407
|
}
|
|
9286
9408
|
function writeProxyLock(proxyPort, metroPort) {
|
|
9287
9409
|
try {
|
|
9288
|
-
|
|
9410
|
+
fs5.writeFileSync(PROXY_LOCK_FILE, JSON.stringify({ pid: process.pid, port: proxyPort, metroPort }));
|
|
9289
9411
|
isPrimaryInstance = true;
|
|
9290
9412
|
logger9.info(`Wrote proxy lock (port ${proxyPort})`);
|
|
9291
9413
|
} catch (err) {
|
|
@@ -9296,9 +9418,9 @@ async function startServer(config) {
|
|
|
9296
9418
|
if (!isPrimaryInstance)
|
|
9297
9419
|
return;
|
|
9298
9420
|
try {
|
|
9299
|
-
const lockData = JSON.parse(
|
|
9421
|
+
const lockData = JSON.parse(fs5.readFileSync(PROXY_LOCK_FILE, "utf8"));
|
|
9300
9422
|
if (lockData.pid === process.pid) {
|
|
9301
|
-
|
|
9423
|
+
fs5.unlinkSync(PROXY_LOCK_FILE);
|
|
9302
9424
|
}
|
|
9303
9425
|
} catch {}
|
|
9304
9426
|
}
|
|
@@ -9334,9 +9456,31 @@ async function startServer(config) {
|
|
|
9334
9456
|
eventsClient.connect(server.host, server.port);
|
|
9335
9457
|
activeDeviceKey = `${server.port}-${target.id}`;
|
|
9336
9458
|
activeDeviceName = target.title || target.deviceName || target.id;
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9459
|
+
if (supportsMultipleDebuggers(target)) {
|
|
9460
|
+
logger9.info("Target supports multiple debuggers (RN 0.85+) — skipping CDP proxy");
|
|
9461
|
+
} else {
|
|
9462
|
+
if (!cdpMultiplexer && config.proxy?.enabled !== false) {
|
|
9463
|
+
const mux = new CDPMultiplexer(cdpSession, { protectedDomains: ["Runtime", "Network"] });
|
|
9464
|
+
try {
|
|
9465
|
+
const startedPort = await mux.start(preferredProxyPort);
|
|
9466
|
+
const devtoolsUrl = mux.getDevToolsUrl();
|
|
9467
|
+
logger9.info(`CDP proxy started on port ${startedPort}`);
|
|
9468
|
+
if (devtoolsUrl)
|
|
9469
|
+
logger9.info(`Chrome DevTools URL: ${devtoolsUrl}`);
|
|
9470
|
+
config.proxy = {
|
|
9471
|
+
...config.proxy,
|
|
9472
|
+
port: startedPort,
|
|
9473
|
+
url: devtoolsUrl
|
|
9474
|
+
};
|
|
9475
|
+
cdpMultiplexer = mux;
|
|
9476
|
+
} catch (err) {
|
|
9477
|
+
logger9.warn("Could not start CDP proxy:", err);
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
const proxyConfig = config.proxy;
|
|
9481
|
+
if (proxyConfig?.port) {
|
|
9482
|
+
writeProxyLock(proxyConfig.port, server.port);
|
|
9483
|
+
}
|
|
9340
9484
|
}
|
|
9341
9485
|
return true;
|
|
9342
9486
|
} catch (err) {
|
|
@@ -9367,31 +9511,13 @@ async function startServer(config) {
|
|
|
9367
9511
|
}, delay2);
|
|
9368
9512
|
}
|
|
9369
9513
|
let cdpMultiplexer = null;
|
|
9370
|
-
|
|
9371
|
-
|
|
9514
|
+
let preferredProxyPort = config.proxy?.port ?? 0;
|
|
9515
|
+
if (preferredProxyPort === 0) {
|
|
9372
9516
|
try {
|
|
9373
|
-
|
|
9374
|
-
if (
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
if (stale.port)
|
|
9378
|
-
preferredProxyPort = stale.port;
|
|
9379
|
-
} catch {}
|
|
9380
|
-
}
|
|
9381
|
-
const proxyPort = await cdpMultiplexer.start(preferredProxyPort);
|
|
9382
|
-
const devtoolsUrl = cdpMultiplexer.getDevToolsUrl();
|
|
9383
|
-
logger9.info(`CDP proxy started on port ${proxyPort}`);
|
|
9384
|
-
if (devtoolsUrl) {
|
|
9385
|
-
logger9.info(`Chrome DevTools URL: ${devtoolsUrl}`);
|
|
9386
|
-
}
|
|
9387
|
-
config.proxy = {
|
|
9388
|
-
...config.proxy,
|
|
9389
|
-
port: proxyPort,
|
|
9390
|
-
url: devtoolsUrl
|
|
9391
|
-
};
|
|
9392
|
-
} catch (err) {
|
|
9393
|
-
logger9.warn("Could not start CDP proxy:", err);
|
|
9394
|
-
}
|
|
9517
|
+
const stale = JSON.parse(fs5.readFileSync(PROXY_LOCK_FILE, "utf8"));
|
|
9518
|
+
if (stale.port)
|
|
9519
|
+
preferredProxyPort = stale.port;
|
|
9520
|
+
} catch {}
|
|
9395
9521
|
}
|
|
9396
9522
|
const transport = new StdioServerTransport;
|
|
9397
9523
|
await mcpServer.connect(transport);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/plugins/devtools.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/plugins/devtools.ts"],"names":[],"mappings":"AA4EA,eAAO,MAAM,cAAc,yCA6FzB,CAAC"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,aAAa,CAAC;AA8DrB,wBAAsB,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,aAAa,CAAC;AA8DrB,wBAAsB,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ZjF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Plugin-based MCP server for React Native/Expo runtime debugging, inspection, and automation via Metro/CDP",
|
|
5
5
|
"homepage": "https://metromcp.dev",
|
|
6
6
|
"repository": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
62
62
|
"chrome-launcher": "^1.2.1",
|
|
63
63
|
"chromium-edge-launcher": "^0.3.0",
|
|
64
|
-
"metro-bridge": "^0.
|
|
64
|
+
"metro-bridge": "^0.2.2",
|
|
65
65
|
"ws": "^8.20.0",
|
|
66
66
|
"zod": "^3.24.4"
|
|
67
67
|
},
|