metro-mcp 0.5.0 → 0.5.2
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 +77 -87
- package/dist/index.d.ts +1 -1
- package/dist/index.js +38 -63
- package/dist/metro/connection.d.ts +1 -1
- package/dist/metro/connection.d.ts.map +1 -1
- package/dist/plugins/simulator.d.ts.map +1 -1
- package/dist/plugins/statusline.d.ts.map +1 -1
- package/dist/plugins/ui-interact.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/bin/metro-mcp.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
// @bun
|
|
1
|
+
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/utils/logger.ts
|
|
5
4
|
function createLogger(name) {
|
|
@@ -99,6 +98,8 @@ function mergeConfig(target, source) {
|
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
// src/server.ts
|
|
101
|
+
import { exec } from "child_process";
|
|
102
|
+
import { promisify } from "util";
|
|
102
103
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
103
104
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
104
105
|
import { z as z20 } from "zod";
|
|
@@ -128,7 +129,7 @@ class CDPClient {
|
|
|
128
129
|
suppressReconnect = false;
|
|
129
130
|
_isConnected = false;
|
|
130
131
|
target = null;
|
|
131
|
-
|
|
132
|
+
lastPingAt = 0;
|
|
132
133
|
requestTimeout = 1e4;
|
|
133
134
|
keepAliveInterval = 1e4;
|
|
134
135
|
async connect(target) {
|
|
@@ -164,7 +165,7 @@ class CDPClient {
|
|
|
164
165
|
const socketForThisConnection = this.ws;
|
|
165
166
|
this.ws.on("open", () => {
|
|
166
167
|
this._isConnected = true;
|
|
167
|
-
this.
|
|
168
|
+
this.lastPingAt = Date.now();
|
|
168
169
|
this.startKeepAlive();
|
|
169
170
|
logger2.info(`Connected to ${this.target?.title || "unknown"}`);
|
|
170
171
|
resolve();
|
|
@@ -190,12 +191,9 @@ class CDPClient {
|
|
|
190
191
|
}
|
|
191
192
|
});
|
|
192
193
|
this.ws.on("ping", () => {
|
|
194
|
+
this.lastPingAt = Date.now();
|
|
193
195
|
logger2.debug("Received ping from Metro");
|
|
194
196
|
});
|
|
195
|
-
this.ws.on("pong", () => {
|
|
196
|
-
this.pongReceived = true;
|
|
197
|
-
logger2.debug("Received pong from Metro");
|
|
198
|
-
});
|
|
199
197
|
} catch (err) {
|
|
200
198
|
reject(err);
|
|
201
199
|
}
|
|
@@ -245,24 +243,16 @@ class CDPClient {
|
|
|
245
243
|
}
|
|
246
244
|
startKeepAlive() {
|
|
247
245
|
this.stopKeepAlive();
|
|
248
|
-
let missedKeepAlives = 0;
|
|
249
246
|
this.keepAliveTimer = setInterval(() => {
|
|
250
247
|
if (!this._isConnected || !this.ws)
|
|
251
248
|
return;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} catch {}
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
} else {
|
|
262
|
-
missedKeepAlives = 0;
|
|
249
|
+
const elapsed = Date.now() - this.lastPingAt;
|
|
250
|
+
if (elapsed > 20000) {
|
|
251
|
+
logger2.warn(`No ping received from Metro in ${elapsed}ms — closing connection`);
|
|
252
|
+
try {
|
|
253
|
+
this.ws.close();
|
|
254
|
+
} catch {}
|
|
263
255
|
}
|
|
264
|
-
this.pongReceived = false;
|
|
265
|
-
this.ws.ping();
|
|
266
256
|
}, this.keepAliveInterval);
|
|
267
257
|
}
|
|
268
258
|
stopKeepAlive() {
|
|
@@ -524,12 +514,12 @@ function extractCDPExceptionMessage(details, fallback = "Evaluation failed") {
|
|
|
524
514
|
// package.json
|
|
525
515
|
var package_default = {
|
|
526
516
|
name: "metro-mcp",
|
|
527
|
-
version: "0.5.
|
|
517
|
+
version: "0.5.2",
|
|
528
518
|
description: "Plugin-based MCP server for React Native/Expo runtime debugging, inspection, and automation via Metro/CDP",
|
|
529
519
|
homepage: "https://github.com/steve228uk/metro-mcp",
|
|
530
520
|
repository: {
|
|
531
521
|
type: "git",
|
|
532
|
-
url: "https://github.com/steve228uk/metro-mcp.git"
|
|
522
|
+
url: "git+https://github.com/steve228uk/metro-mcp.git"
|
|
533
523
|
},
|
|
534
524
|
bugs: {
|
|
535
525
|
url: "https://github.com/steve228uk/metro-mcp/issues"
|
|
@@ -565,7 +555,7 @@ var package_default = {
|
|
|
565
555
|
clean: "rm -rf dist",
|
|
566
556
|
build: "bun run clean && bun run build:js && bun run build:bin && bun run build:types",
|
|
567
557
|
"build:js": "bun build src/index.ts src/plugin.ts --outdir dist --target node --external @modelcontextprotocol/sdk --external zod --external ws && bun build src/client/index.ts --outfile dist/client/index.js --target browser --external @modelcontextprotocol/sdk --external zod && bun build src/client/index.ts --outfile dist/client/index.cjs --target browser --format cjs --external @modelcontextprotocol/sdk --external zod && bun build src/plugin.ts --outfile dist/plugin.cjs --target node --format cjs --external @modelcontextprotocol/sdk --external zod",
|
|
568
|
-
"build:bin": "bun build bin/metro-mcp.ts --outfile dist/bin/metro-mcp.js --target
|
|
558
|
+
"build:bin": "bun build bin/metro-mcp.ts --outfile dist/bin/metro-mcp.js --target node --external @modelcontextprotocol/sdk --external zod --external ws && chmod +x dist/bin/metro-mcp.js",
|
|
569
559
|
"build:types": "bunx tsc -p tsconfig.build.json",
|
|
570
560
|
typecheck: "tsc --noEmit"
|
|
571
561
|
},
|
|
@@ -698,7 +688,7 @@ var consolePlugin = definePlugin({
|
|
|
698
688
|
buffer.push({
|
|
699
689
|
timestamp: Date.now(),
|
|
700
690
|
level: "info",
|
|
701
|
-
message: "
|
|
691
|
+
message: "── Metro rebuilding ── (file change detected)"
|
|
702
692
|
});
|
|
703
693
|
}
|
|
704
694
|
});
|
|
@@ -706,7 +696,7 @@ var consolePlugin = definePlugin({
|
|
|
706
696
|
buffer.push({
|
|
707
697
|
timestamp: Date.now(),
|
|
708
698
|
level: "info",
|
|
709
|
-
message: "
|
|
699
|
+
message: "── CDP reconnected ── (logs during the disconnection gap may be missing)"
|
|
710
700
|
});
|
|
711
701
|
});
|
|
712
702
|
ctx.registerTool("get_console_logs", {
|
|
@@ -864,7 +854,7 @@ var networkPlugin = definePlugin({
|
|
|
864
854
|
return result.map((r) => {
|
|
865
855
|
const duration = r.endTime ? `${r.endTime - r.startTime}ms` : "pending";
|
|
866
856
|
const status = r.error ? `ERR: ${r.error}` : `${r.status || "???"}`;
|
|
867
|
-
return `${r.method} ${r.url}
|
|
857
|
+
return `${r.method} ${r.url} → ${status} (${duration})`;
|
|
868
858
|
}).join(`
|
|
869
859
|
`);
|
|
870
860
|
}
|
|
@@ -1627,7 +1617,7 @@ var componentsPlugin = definePlugin({
|
|
|
1627
1617
|
}
|
|
1628
1618
|
});
|
|
1629
1619
|
ctx.registerTool("get_testable_elements", {
|
|
1630
|
-
description: "Get all elements with testID or accessibilityLabel
|
|
1620
|
+
description: "Get all elements with testID or accessibilityLabel — useful for test generation.",
|
|
1631
1621
|
parameters: z8.object({}),
|
|
1632
1622
|
handler: async () => {
|
|
1633
1623
|
const options = JSON.stringify({
|
|
@@ -1749,6 +1739,8 @@ var storagePlugin = definePlugin({
|
|
|
1749
1739
|
});
|
|
1750
1740
|
|
|
1751
1741
|
// src/plugins/simulator.ts
|
|
1742
|
+
import { readFile } from "fs/promises";
|
|
1743
|
+
import { existsSync } from "fs";
|
|
1752
1744
|
import { z as z10 } from "zod";
|
|
1753
1745
|
var simulatorPlugin = definePlugin({
|
|
1754
1746
|
name: "simulator",
|
|
@@ -1780,10 +1772,9 @@ var simulatorPlugin = definePlugin({
|
|
|
1780
1772
|
} else {
|
|
1781
1773
|
await ctx.exec(`adb exec-out screencap -p > "${tmpFile}"`);
|
|
1782
1774
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
const
|
|
1786
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
1775
|
+
if (existsSync(tmpFile)) {
|
|
1776
|
+
const buffer = await readFile(tmpFile);
|
|
1777
|
+
const base64 = buffer.toString("base64");
|
|
1787
1778
|
await ctx.exec(`rm -f "${tmpFile}"`);
|
|
1788
1779
|
return {
|
|
1789
1780
|
type: "image",
|
|
@@ -2027,6 +2018,7 @@ var deeplinkPlugin = definePlugin({
|
|
|
2027
2018
|
});
|
|
2028
2019
|
|
|
2029
2020
|
// src/plugins/ui-interact.ts
|
|
2021
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2030
2022
|
import { z as z12 } from "zod";
|
|
2031
2023
|
|
|
2032
2024
|
// src/utils/fiber.ts
|
|
@@ -2146,7 +2138,7 @@ var uiInteractPlugin = definePlugin({
|
|
|
2146
2138
|
}
|
|
2147
2139
|
const IDB_INSTALL = "Install IDB with: brew install idb-companion";
|
|
2148
2140
|
ctx.registerTool("list_elements", {
|
|
2149
|
-
description: "Get interactive elements from the current screen via the React component tree. Returns labels, testIDs, and roles
|
|
2141
|
+
description: "Get interactive elements from the current screen via the React component tree. Returns labels, testIDs, and roles — use label or testID with tap_element.",
|
|
2150
2142
|
parameters: z12.object({
|
|
2151
2143
|
interactiveOnly: z12.boolean().default(false).describe("Return only elements with onPress handlers")
|
|
2152
2144
|
}),
|
|
@@ -2273,7 +2265,7 @@ var uiInteractPlugin = definePlugin({
|
|
|
2273
2265
|
let content = "";
|
|
2274
2266
|
try {
|
|
2275
2267
|
await ctx.exec(`adb shell uiautomator dump /sdcard/uidump.xml && adb pull /sdcard/uidump.xml ${tmpFile} 2>/dev/null`);
|
|
2276
|
-
content = await
|
|
2268
|
+
content = await readFile2(tmpFile, "utf8");
|
|
2277
2269
|
} finally {
|
|
2278
2270
|
await ctx.exec(`rm -f ${tmpFile}`).catch(() => {});
|
|
2279
2271
|
}
|
|
@@ -3100,9 +3092,9 @@ var commandsPlugin = definePlugin({
|
|
|
3100
3092
|
|
|
3101
3093
|
// src/plugins/test-recorder.ts
|
|
3102
3094
|
import { z as z16 } from "zod";
|
|
3103
|
-
import { readdir, readFile, writeFile, mkdir } from "fs/promises";
|
|
3104
|
-
import { homedir } from "os";
|
|
3105
|
-
import { join } from "path";
|
|
3095
|
+
import { readdir, readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
|
|
3096
|
+
import { homedir } from "node:os";
|
|
3097
|
+
import { join } from "node:path";
|
|
3106
3098
|
var CURRENT_ROUTE_JS = `
|
|
3107
3099
|
(function() {
|
|
3108
3100
|
try {
|
|
@@ -3125,7 +3117,7 @@ var START_RECORDING_JS = `
|
|
|
3125
3117
|
|
|
3126
3118
|
${GET_ROUTE_FUNC_JS}
|
|
3127
3119
|
|
|
3128
|
-
//
|
|
3120
|
+
// ── Intercept Object.freeze: wrap event handlers before React freezes props ──
|
|
3129
3121
|
var origFreeze = Object.freeze;
|
|
3130
3122
|
Object.freeze = function(obj) {
|
|
3131
3123
|
if (globalThis.__METRO_MCP_REC_ACTIVE__ && obj && typeof obj === 'object' && !Array.isArray(obj) && !obj.__mcpRec) {
|
|
@@ -3223,7 +3215,7 @@ var START_RECORDING_JS = `
|
|
|
3223
3215
|
return origFreeze.call(this, obj);
|
|
3224
3216
|
};
|
|
3225
3217
|
|
|
3226
|
-
//
|
|
3218
|
+
// ── Force re-render of already-mounted scroll containers ────────────────────
|
|
3227
3219
|
// Object.freeze only fires on future renders. For scroll views mounted before
|
|
3228
3220
|
// recording started, we trigger a one-time re-render so our freeze interceptor
|
|
3229
3221
|
// can wrap their handlers.
|
|
@@ -3235,7 +3227,7 @@ var START_RECORDING_JS = `
|
|
|
3235
3227
|
var cn = typeof fiber.type === 'string'
|
|
3236
3228
|
? fiber.type
|
|
3237
3229
|
: (fiber.type && (fiber.type.displayName || fiber.type.name)) || '';
|
|
3238
|
-
// String checks before regex
|
|
3230
|
+
// String checks before regex — faster for the common case
|
|
3239
3231
|
if (cn === 'ScrollView' || cn === 'FlatList' || cn === 'SectionList' ||
|
|
3240
3232
|
cn === 'VirtualizedList' || cn === 'FlashList' || cn === 'BigList' ||
|
|
3241
3233
|
cn === 'RecyclerListView' || cn === 'MasonryFlashList') return true;
|
|
@@ -3275,7 +3267,7 @@ var START_RECORDING_JS = `
|
|
|
3275
3267
|
}
|
|
3276
3268
|
})();
|
|
3277
3269
|
|
|
3278
|
-
//
|
|
3270
|
+
// ── Track navigation events on every React commit ───────────────────────────
|
|
3279
3271
|
var origCommit = hook.onCommitFiberRoot;
|
|
3280
3272
|
hook.onCommitFiberRoot = function(id, root) {
|
|
3281
3273
|
if (globalThis.__METRO_MCP_REC_ACTIVE__) {
|
|
@@ -3360,7 +3352,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
3360
3352
|
description: "Unified mobile test recorder: captures taps, text entry, swipes and navigation via fiber patching; generates Appium, Maestro, and Detox tests",
|
|
3361
3353
|
async setup(ctx) {
|
|
3362
3354
|
ctx.registerTool("start_test_recording", {
|
|
3363
|
-
description: "Inject interaction interceptors into the running app via the React fiber tree. " + "Captures taps, text entry, long presses, keyboard submits, and scroll/swipe gestures
|
|
3355
|
+
description: "Inject interaction interceptors into the running app via the React fiber tree. " + "Captures taps, text entry, long presses, keyboard submits, and scroll/swipe gestures — " + "with no changes to your app code. Works with ScrollView, FlatList, SectionList, " + "FlashList, and other scroll containers. " + "Call stop_test_recording when done, then generate_test_from_recording to get the test.",
|
|
3364
3356
|
parameters: z16.object({}),
|
|
3365
3357
|
handler: async () => {
|
|
3366
3358
|
storedEvents = null;
|
|
@@ -3373,7 +3365,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
3373
3365
|
injected = false;
|
|
3374
3366
|
}
|
|
3375
3367
|
if (!injected) {
|
|
3376
|
-
return `Could not inject recording hooks
|
|
3368
|
+
return `Could not inject recording hooks — ${injectError}`;
|
|
3377
3369
|
}
|
|
3378
3370
|
const route = await ctx.evalInApp(CURRENT_ROUTE_JS, { timeout: 3000 }).catch(() => null);
|
|
3379
3371
|
const routeInfo = route ? ` on screen "${route}"` : "";
|
|
@@ -3562,7 +3554,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
3562
3554
|
const filePath = join(RECORDINGS_DIR, `${safe}.json`);
|
|
3563
3555
|
let raw;
|
|
3564
3556
|
try {
|
|
3565
|
-
raw = await
|
|
3557
|
+
raw = await readFile3(filePath, "utf8");
|
|
3566
3558
|
} catch {
|
|
3567
3559
|
return `Recording not found: ${filePath}. Call list_test_recordings to see available files.`;
|
|
3568
3560
|
}
|
|
@@ -3883,13 +3875,13 @@ var MAX_DEPTH = 8;
|
|
|
3883
3875
|
var MIN_PERCENT = 0.5;
|
|
3884
3876
|
function bar(value, max) {
|
|
3885
3877
|
const filled = max > 0 ? Math.max(1, Math.round(value / max * BAR_WIDTH)) : 0;
|
|
3886
|
-
return "
|
|
3878
|
+
return "█".repeat(filled).padEnd(BAR_WIDTH);
|
|
3887
3879
|
}
|
|
3888
3880
|
function barPct(pct) {
|
|
3889
|
-
return "
|
|
3881
|
+
return "█".repeat(Math.max(1, Math.round(pct / 100 * BAR_WIDTH))).padEnd(BAR_WIDTH);
|
|
3890
3882
|
}
|
|
3891
3883
|
function trunc(s, max) {
|
|
3892
|
-
return s.length > max ? s.slice(0, max - 1) + "
|
|
3884
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
3893
3885
|
}
|
|
3894
3886
|
function memoSavings(r) {
|
|
3895
3887
|
return r.baseDuration > 0 ? parseFloat(((r.baseDuration - r.actualDuration) / r.baseDuration * 100).toFixed(1)) : null;
|
|
@@ -3924,7 +3916,7 @@ function buildCpuFlamegraph(profile, analysis) {
|
|
|
3924
3916
|
const label = trunc(fnName, 30).padEnd(30);
|
|
3925
3917
|
const ms = parseFloat(((hasChildren ? total : self) / (sampleCount || 1) * durationMs).toFixed(1));
|
|
3926
3918
|
const pct = hasChildren ? totalPct : selfPct;
|
|
3927
|
-
lines.push(`${indent}${hasChildren ? "
|
|
3919
|
+
lines.push(`${indent}${hasChildren ? "▼" : "■"} ${label} ${pct.toFixed(1).padStart(5)}% ${barPct(pct)} ${ms}ms ${hasChildren ? "total" : "self"}`);
|
|
3928
3920
|
for (const c of node.children ?? [])
|
|
3929
3921
|
renderNode(c, depth + 1);
|
|
3930
3922
|
}
|
|
@@ -3975,7 +3967,7 @@ function buildRenderChart(renders) {
|
|
|
3975
3967
|
const lines = [];
|
|
3976
3968
|
const sorted = [...renders].sort((a, b) => b.actualDuration - a.actualDuration);
|
|
3977
3969
|
const maxActual = sorted[0]?.actualDuration ?? 1;
|
|
3978
|
-
lines.push("=== React Renders
|
|
3970
|
+
lines.push("=== React Renders — Ranked by Actual Duration ===");
|
|
3979
3971
|
const hdr = ` # ${"Component".padEnd(26)} ${"Phase".padEnd(14)} ${"Actual".padStart(8)} ${"Base".padStart(8)} Savings Chart`;
|
|
3980
3972
|
lines.push(hdr);
|
|
3981
3973
|
lines.push("-".repeat(hdr.length + BAR_WIDTH));
|
|
@@ -4015,7 +4007,7 @@ var DEVTOOLS_START_EXPR = `(function() {
|
|
|
4015
4007
|
}
|
|
4016
4008
|
if (count > 0) return { ok: true, method: 'startProfiling', count: count };
|
|
4017
4009
|
|
|
4018
|
-
// Path 2: patch onCommitFiberRoot
|
|
4010
|
+
// Path 2: patch onCommitFiberRoot — works without DevTools backend.
|
|
4019
4011
|
// React calls this on every commit; fiber.actualDuration is tracked in dev builds.
|
|
4020
4012
|
if (typeof hook.onCommitFiberRoot === 'undefined') return { error: 'no-hook-method' };
|
|
4021
4013
|
var orig = hook.onCommitFiberRoot;
|
|
@@ -4136,7 +4128,7 @@ var profilerPlugin = definePlugin({
|
|
|
4136
4128
|
} else if (lastCpuProfile && lastCpuAnalysis) {
|
|
4137
4129
|
sections.push(buildCpuFlamegraph(lastCpuProfile, lastCpuAnalysis));
|
|
4138
4130
|
} else {
|
|
4139
|
-
sections.push("(no profile
|
|
4131
|
+
sections.push("(no profile — call start_profiling, interact, then stop_profiling)");
|
|
4140
4132
|
}
|
|
4141
4133
|
sections.push("");
|
|
4142
4134
|
try {
|
|
@@ -4151,7 +4143,7 @@ ${NOT_SETUP_MSG}`);
|
|
|
4151
4143
|
`);
|
|
4152
4144
|
}
|
|
4153
4145
|
ctx.registerTool("start_profiling", {
|
|
4154
|
-
description: "Start profiling the running React Native app. " + "Primary path: injects into the React DevTools hook (__REACT_DEVTOOLS_GLOBAL_HOOK__) via evalInApp
|
|
4146
|
+
description: "Start profiling the running React Native app. " + "Primary path: injects into the React DevTools hook (__REACT_DEVTOOLS_GLOBAL_HOOK__) via evalInApp — " + "captures all component render durations without requiring <Profiler> wrappers, works on all architectures. " + "Fallback (legacy arch only): CDP Profiler domain for JS CPU call-graph sampling. " + "Perform the interaction you want to measure, then call stop_profiling.",
|
|
4155
4147
|
parameters: z17.object({
|
|
4156
4148
|
samplingInterval: z17.number().int().min(100).max(1e5).default(1000).describe("CDP fallback only: sampling interval in microseconds (default 1000).")
|
|
4157
4149
|
}),
|
|
@@ -4183,7 +4175,7 @@ ${NOT_SETUP_MSG}`);
|
|
|
4183
4175
|
await ctx.cdp.send("Profiler.setSamplingInterval", { interval: samplingInterval });
|
|
4184
4176
|
await ctx.cdp.send("Profiler.start");
|
|
4185
4177
|
profilingMode = "cdp";
|
|
4186
|
-
return `Profiling started via CDP (sampling every ${samplingInterval}
|
|
4178
|
+
return `Profiling started via CDP (sampling every ${samplingInterval} µs). Perform the interaction you want to measure, then call stop_profiling.`;
|
|
4187
4179
|
} catch (cdpErr) {
|
|
4188
4180
|
const msg = cdpErr instanceof Error ? cdpErr.message : String(cdpErr);
|
|
4189
4181
|
if (!msg.includes("Unsupported method") && !msg.includes("not supported")) {
|
|
@@ -4195,7 +4187,7 @@ ${NOT_SETUP_MSG}`);
|
|
|
4195
4187
|
profilingMode = "console";
|
|
4196
4188
|
return "Profiling started via console.profile(). Perform the interaction you want to measure, then call stop_profiling.";
|
|
4197
4189
|
} catch (consoleErr) {
|
|
4198
|
-
return `Failed to start profiling: all paths exhausted
|
|
4190
|
+
return `Failed to start profiling: all paths exhausted — ${consoleErr instanceof Error ? consoleErr.message : String(consoleErr)}`;
|
|
4199
4191
|
}
|
|
4200
4192
|
}
|
|
4201
4193
|
});
|
|
@@ -4213,7 +4205,7 @@ ${NOT_SETUP_MSG}`);
|
|
|
4213
4205
|
const raw = await ctx.evalInApp(DEVTOOLS_STOP_EXPR);
|
|
4214
4206
|
profilingMode = null;
|
|
4215
4207
|
if (!raw || raw.length === 0) {
|
|
4216
|
-
return { mode: "devtools-hook", commitCount: 0, message: "No commits recorded
|
|
4208
|
+
return { mode: "devtools-hook", commitCount: 0, message: "No commits recorded — profiling window may be too short." };
|
|
4217
4209
|
}
|
|
4218
4210
|
lastDevToolsProfile = raw;
|
|
4219
4211
|
const byName = new Map;
|
|
@@ -4389,7 +4381,7 @@ ${NOT_SETUP_MSG}`);
|
|
|
4389
4381
|
const raw = await ctx.evalInApp(DEVTOOLS_STOP_EXPR);
|
|
4390
4382
|
profilingMode = null;
|
|
4391
4383
|
if (!raw || raw.length === 0) {
|
|
4392
|
-
return { mode: "devtools-hook", commitCount: 0, message: "No commits recorded
|
|
4384
|
+
return { mode: "devtools-hook", commitCount: 0, message: "No commits recorded — expression may have been too fast." };
|
|
4393
4385
|
}
|
|
4394
4386
|
lastDevToolsProfile = raw;
|
|
4395
4387
|
const byName = new Map;
|
|
@@ -4566,11 +4558,11 @@ var promptsPlugin = definePlugin({
|
|
|
4566
4558
|
a. Stop CPU profiling and get the analysis (stop_profiling)
|
|
4567
4559
|
b. Read React render timings (get_react_renders)
|
|
4568
4560
|
c. Read the flamegraph resource (metro://profiler/flamegraph) for a combined visual breakdown
|
|
4569
|
-
d. Check for slow network requests (search_network
|
|
4561
|
+
d. Check for slow network requests (search_network — look for responses > 1s)
|
|
4570
4562
|
e. Check console logs for perf warnings (get_console_logs with search="slow" or "perf")
|
|
4571
4563
|
6. Summarize:
|
|
4572
|
-
- Top JS CPU hotspots by self time
|
|
4573
|
-
- Slowest React component renders and whether memoization (memo/useMemo) is helping
|
|
4564
|
+
- Top JS CPU hotspots by self time — which function and file is burning the most CPU
|
|
4565
|
+
- Slowest React component renders and whether memoization (memo/useMemo) is helping — compare actualDuration vs baseDuration
|
|
4574
4566
|
- Components that re-render frequently (high count in the summary)
|
|
4575
4567
|
- Any slow network requests contributing to perceived slowness
|
|
4576
4568
|
- Concrete, prioritised recommendations`
|
|
@@ -4628,7 +4620,7 @@ var promptsPlugin = definePlugin({
|
|
|
4628
4620
|
handler: async (args) => {
|
|
4629
4621
|
const format = args.format || "appium";
|
|
4630
4622
|
const platform = args.platform || "ios";
|
|
4631
|
-
const bundleIdNote = args.bundleId ? `bundleId: "${args.bundleId}"` : "bundleId: not provided
|
|
4623
|
+
const bundleIdNote = args.bundleId ? `bundleId: "${args.bundleId}"` : "bundleId: not provided — you will need to add it manually to the generated test or pass it as a parameter";
|
|
4632
4624
|
return [
|
|
4633
4625
|
{
|
|
4634
4626
|
role: "user",
|
|
@@ -4713,7 +4705,7 @@ Please follow this systematic process:
|
|
|
4713
4705
|
6. **Capture allocations**: Call stop_heap_sampling to see which functions allocated the most memory
|
|
4714
4706
|
7. **Final memory**: Call get_memory_info to measure total heap growth
|
|
4715
4707
|
8. **Analysis**:
|
|
4716
|
-
- Compare baseline vs final heap usage
|
|
4708
|
+
- Compare baseline vs final heap usage — how much did it grow?
|
|
4717
4709
|
- Which functions in the heap sampling report are allocating unexpectedly (look for non-GC-friendly objects)?
|
|
4718
4710
|
- Check if any components from the scenario appear in the top allocations
|
|
4719
4711
|
- Look for retained closures, large arrays, or repeated DOM/fiber allocations
|
|
@@ -4736,7 +4728,7 @@ Please follow these steps:
|
|
|
4736
4728
|
|
|
4737
4729
|
1. **Collect errors**: Call get_errors to retrieve all recent exceptions with stack traces
|
|
4738
4730
|
2. **Symbolicate**: For each error, call symbolicate with the stack trace to get readable file/line references
|
|
4739
|
-
3. **Check logs**: Call get_console_logs
|
|
4731
|
+
3. **Check logs**: Call get_console_logs — look for warnings or errors immediately before the crash timestamp
|
|
4740
4732
|
4. **Current state**:
|
|
4741
4733
|
- Call get_current_route to see what screen the app is on
|
|
4742
4734
|
- Call take_screenshot to capture the current visual state
|
|
@@ -4786,7 +4778,7 @@ var automationPlugin = definePlugin({
|
|
|
4786
4778
|
description: "Wait and polling tools for reliable E2E automation with async state changes",
|
|
4787
4779
|
async setup(ctx) {
|
|
4788
4780
|
ctx.registerTool("wait_for_element", {
|
|
4789
|
-
description: "Poll the component tree until an element matching the given testID or accessibilityLabel appears. " + "Returns element info on success. Use after tap_element, navigate(), or any action that triggers " + "async screen transitions or data loading
|
|
4781
|
+
description: "Poll the component tree until an element matching the given testID or accessibilityLabel appears. " + "Returns element info on success. Use after tap_element, navigate(), or any action that triggers " + "async screen transitions or data loading — instead of immediately calling the next tool.",
|
|
4790
4782
|
parameters: z18.object({
|
|
4791
4783
|
selector: z18.string().describe("testID or accessibilityLabel to wait for"),
|
|
4792
4784
|
timeout: z18.number().int().min(100).max(60000).default(1e4).describe("Maximum wait time in milliseconds (default 10000)"),
|
|
@@ -4853,9 +4845,9 @@ var automationPlugin = definePlugin({
|
|
|
4853
4845
|
});
|
|
4854
4846
|
|
|
4855
4847
|
// src/plugins/statusline.ts
|
|
4856
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
4857
|
-
import { homedir as homedir2 } from "os";
|
|
4858
|
-
import { join as join2 } from "path";
|
|
4848
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
4849
|
+
import { homedir as homedir2 } from "node:os";
|
|
4850
|
+
import { join as join2 } from "node:path";
|
|
4859
4851
|
import { z as z19 } from "zod";
|
|
4860
4852
|
var STATUS_FILE = "/tmp/metro-mcp-status.json";
|
|
4861
4853
|
var CLAUDE_DIR = join2(homedir2(), ".claude");
|
|
@@ -4863,12 +4855,12 @@ var SCRIPT_PATH = join2(CLAUDE_DIR, "metro-mcp-statusline.sh");
|
|
|
4863
4855
|
var SCRIPT_CONTENT = `#!/bin/bash
|
|
4864
4856
|
# Metro MCP status line for Claude Code
|
|
4865
4857
|
# Shows CDP connection status of the Metro bundler.
|
|
4866
|
-
# Auto-generated by metro-mcp
|
|
4858
|
+
# Auto-generated by metro-mcp — re-run setup_statusline to regenerate.
|
|
4867
4859
|
|
|
4868
4860
|
STATUS_FILE="/tmp/metro-mcp-status.json"
|
|
4869
4861
|
|
|
4870
4862
|
if [ ! -f "$STATUS_FILE" ]; then
|
|
4871
|
-
printf "\\033[2mMetro
|
|
4863
|
+
printf "\\033[2mMetro ○\\033[0m\\n"
|
|
4872
4864
|
exit 0
|
|
4873
4865
|
fi
|
|
4874
4866
|
|
|
@@ -4877,9 +4869,9 @@ HOST=$(jq -r '.host' "$STATUS_FILE" 2>/dev/null)
|
|
|
4877
4869
|
PORT=$(jq -r '.port' "$STATUS_FILE" 2>/dev/null)
|
|
4878
4870
|
|
|
4879
4871
|
if [ "$CONNECTED" = "true" ]; then
|
|
4880
|
-
printf "\\033[32mMetro
|
|
4872
|
+
printf "\\033[32mMetro ● %s:%s\\033[0m\\n" "$HOST" "$PORT"
|
|
4881
4873
|
else
|
|
4882
|
-
printf "\\033[31mMetro
|
|
4874
|
+
printf "\\033[31mMetro ●\\033[0m\\n"
|
|
4883
4875
|
fi
|
|
4884
4876
|
`;
|
|
4885
4877
|
function writeStatus(data) {
|
|
@@ -4891,7 +4883,12 @@ var statuslinePlugin = definePlugin({
|
|
|
4891
4883
|
name: "statusline",
|
|
4892
4884
|
description: "Writes CDP connection state to a file for Claude Code status bar integration",
|
|
4893
4885
|
async setup(ctx) {
|
|
4894
|
-
|
|
4886
|
+
let lastConnected = null;
|
|
4887
|
+
function write() {
|
|
4888
|
+
const connected = ctx.cdp.isConnected();
|
|
4889
|
+
if (connected === lastConnected)
|
|
4890
|
+
return;
|
|
4891
|
+
lastConnected = connected;
|
|
4895
4892
|
const target = ctx.cdp.getTarget();
|
|
4896
4893
|
writeStatus({
|
|
4897
4894
|
connected,
|
|
@@ -4901,11 +4898,12 @@ var statuslinePlugin = definePlugin({
|
|
|
4901
4898
|
updatedAt: Date.now()
|
|
4902
4899
|
});
|
|
4903
4900
|
}
|
|
4904
|
-
ctx.cdp.on("reconnected",
|
|
4905
|
-
ctx.cdp.on("disconnected",
|
|
4906
|
-
write
|
|
4901
|
+
ctx.cdp.on("reconnected", write);
|
|
4902
|
+
ctx.cdp.on("disconnected", write);
|
|
4903
|
+
setInterval(write, 5000);
|
|
4904
|
+
write();
|
|
4907
4905
|
ctx.registerTool("setup_statusline", {
|
|
4908
|
-
description: "Writes the Metro CDP connection status script to ~/.claude/metro-mcp-statusline.sh. " + "Does not modify settings.json
|
|
4906
|
+
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.",
|
|
4909
4907
|
parameters: z19.object({}),
|
|
4910
4908
|
handler: async () => {
|
|
4911
4909
|
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
@@ -4923,6 +4921,7 @@ var statuslinePlugin = definePlugin({
|
|
|
4923
4921
|
});
|
|
4924
4922
|
|
|
4925
4923
|
// src/server.ts
|
|
4924
|
+
var execAsync = promisify(exec);
|
|
4926
4925
|
var logger6 = createLogger("server");
|
|
4927
4926
|
var BUILT_IN_PLUGINS = [
|
|
4928
4927
|
consolePlugin,
|
|
@@ -5094,16 +5093,7 @@ async function startServer(config) {
|
|
|
5094
5093
|
}
|
|
5095
5094
|
},
|
|
5096
5095
|
exec: async (command) => {
|
|
5097
|
-
const
|
|
5098
|
-
stdout: "pipe",
|
|
5099
|
-
stderr: "pipe"
|
|
5100
|
-
});
|
|
5101
|
-
const stdout = await new Response(proc.stdout).text();
|
|
5102
|
-
const stderr = await new Response(proc.stderr).text();
|
|
5103
|
-
await proc.exited;
|
|
5104
|
-
if (proc.exitCode !== 0) {
|
|
5105
|
-
throw new Error(stderr || `Command failed with exit code ${proc.exitCode}`);
|
|
5106
|
-
}
|
|
5096
|
+
const { stdout } = await execAsync(command);
|
|
5107
5097
|
return stdout;
|
|
5108
5098
|
},
|
|
5109
5099
|
format: formatUtils
|
|
@@ -5198,7 +5188,7 @@ async function main() {
|
|
|
5198
5188
|
const args = process.argv.slice(2);
|
|
5199
5189
|
if (args.includes("--help") || args.includes("-h")) {
|
|
5200
5190
|
console.error(`
|
|
5201
|
-
metro-mcp
|
|
5191
|
+
metro-mcp — React Native MCP Server
|
|
5202
5192
|
|
|
5203
5193
|
Usage:
|
|
5204
5194
|
metro-mcp [options]
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
// @bun
|
|
1
|
+
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/plugin.ts
|
|
5
4
|
function definePlugin(plugin) {
|
|
@@ -107,6 +106,8 @@ function mergeConfig(target, source) {
|
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
// src/server.ts
|
|
109
|
+
import { exec } from "child_process";
|
|
110
|
+
import { promisify } from "util";
|
|
110
111
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
111
112
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
112
113
|
import { z as z20 } from "zod";
|
|
@@ -136,7 +137,7 @@ class CDPClient {
|
|
|
136
137
|
suppressReconnect = false;
|
|
137
138
|
_isConnected = false;
|
|
138
139
|
target = null;
|
|
139
|
-
|
|
140
|
+
lastPingAt = 0;
|
|
140
141
|
requestTimeout = 1e4;
|
|
141
142
|
keepAliveInterval = 1e4;
|
|
142
143
|
async connect(target) {
|
|
@@ -172,7 +173,7 @@ class CDPClient {
|
|
|
172
173
|
const socketForThisConnection = this.ws;
|
|
173
174
|
this.ws.on("open", () => {
|
|
174
175
|
this._isConnected = true;
|
|
175
|
-
this.
|
|
176
|
+
this.lastPingAt = Date.now();
|
|
176
177
|
this.startKeepAlive();
|
|
177
178
|
logger2.info(`Connected to ${this.target?.title || "unknown"}`);
|
|
178
179
|
resolve();
|
|
@@ -198,12 +199,9 @@ class CDPClient {
|
|
|
198
199
|
}
|
|
199
200
|
});
|
|
200
201
|
this.ws.on("ping", () => {
|
|
202
|
+
this.lastPingAt = Date.now();
|
|
201
203
|
logger2.debug("Received ping from Metro");
|
|
202
204
|
});
|
|
203
|
-
this.ws.on("pong", () => {
|
|
204
|
-
this.pongReceived = true;
|
|
205
|
-
logger2.debug("Received pong from Metro");
|
|
206
|
-
});
|
|
207
205
|
} catch (err) {
|
|
208
206
|
reject(err);
|
|
209
207
|
}
|
|
@@ -253,24 +251,16 @@ class CDPClient {
|
|
|
253
251
|
}
|
|
254
252
|
startKeepAlive() {
|
|
255
253
|
this.stopKeepAlive();
|
|
256
|
-
let missedKeepAlives = 0;
|
|
257
254
|
this.keepAliveTimer = setInterval(() => {
|
|
258
255
|
if (!this._isConnected || !this.ws)
|
|
259
256
|
return;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch {}
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
missedKeepAlives = 0;
|
|
257
|
+
const elapsed = Date.now() - this.lastPingAt;
|
|
258
|
+
if (elapsed > 20000) {
|
|
259
|
+
logger2.warn(`No ping received from Metro in ${elapsed}ms — closing connection`);
|
|
260
|
+
try {
|
|
261
|
+
this.ws.close();
|
|
262
|
+
} catch {}
|
|
271
263
|
}
|
|
272
|
-
this.pongReceived = false;
|
|
273
|
-
this.ws.ping();
|
|
274
264
|
}, this.keepAliveInterval);
|
|
275
265
|
}
|
|
276
266
|
stopKeepAlive() {
|
|
@@ -532,12 +522,12 @@ function extractCDPExceptionMessage(details, fallback = "Evaluation failed") {
|
|
|
532
522
|
// package.json
|
|
533
523
|
var package_default = {
|
|
534
524
|
name: "metro-mcp",
|
|
535
|
-
version: "0.5.
|
|
525
|
+
version: "0.5.2",
|
|
536
526
|
description: "Plugin-based MCP server for React Native/Expo runtime debugging, inspection, and automation via Metro/CDP",
|
|
537
527
|
homepage: "https://github.com/steve228uk/metro-mcp",
|
|
538
528
|
repository: {
|
|
539
529
|
type: "git",
|
|
540
|
-
url: "https://github.com/steve228uk/metro-mcp.git"
|
|
530
|
+
url: "git+https://github.com/steve228uk/metro-mcp.git"
|
|
541
531
|
},
|
|
542
532
|
bugs: {
|
|
543
533
|
url: "https://github.com/steve228uk/metro-mcp/issues"
|
|
@@ -573,7 +563,7 @@ var package_default = {
|
|
|
573
563
|
clean: "rm -rf dist",
|
|
574
564
|
build: "bun run clean && bun run build:js && bun run build:bin && bun run build:types",
|
|
575
565
|
"build:js": "bun build src/index.ts src/plugin.ts --outdir dist --target node --external @modelcontextprotocol/sdk --external zod --external ws && bun build src/client/index.ts --outfile dist/client/index.js --target browser --external @modelcontextprotocol/sdk --external zod && bun build src/client/index.ts --outfile dist/client/index.cjs --target browser --format cjs --external @modelcontextprotocol/sdk --external zod && bun build src/plugin.ts --outfile dist/plugin.cjs --target node --format cjs --external @modelcontextprotocol/sdk --external zod",
|
|
576
|
-
"build:bin": "bun build bin/metro-mcp.ts --outfile dist/bin/metro-mcp.js --target
|
|
566
|
+
"build:bin": "bun build bin/metro-mcp.ts --outfile dist/bin/metro-mcp.js --target node --external @modelcontextprotocol/sdk --external zod --external ws && chmod +x dist/bin/metro-mcp.js",
|
|
577
567
|
"build:types": "bunx tsc -p tsconfig.build.json",
|
|
578
568
|
typecheck: "tsc --noEmit"
|
|
579
569
|
},
|
|
@@ -1752,6 +1742,8 @@ var storagePlugin = definePlugin({
|
|
|
1752
1742
|
});
|
|
1753
1743
|
|
|
1754
1744
|
// src/plugins/simulator.ts
|
|
1745
|
+
import { readFile } from "fs/promises";
|
|
1746
|
+
import { existsSync } from "fs";
|
|
1755
1747
|
import { z as z10 } from "zod";
|
|
1756
1748
|
var simulatorPlugin = definePlugin({
|
|
1757
1749
|
name: "simulator",
|
|
@@ -1783,10 +1775,9 @@ var simulatorPlugin = definePlugin({
|
|
|
1783
1775
|
} else {
|
|
1784
1776
|
await ctx.exec(`adb exec-out screencap -p > "${tmpFile}"`);
|
|
1785
1777
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
1778
|
+
if (existsSync(tmpFile)) {
|
|
1779
|
+
const buffer = await readFile(tmpFile);
|
|
1780
|
+
const base64 = buffer.toString("base64");
|
|
1790
1781
|
await ctx.exec(`rm -f "${tmpFile}"`);
|
|
1791
1782
|
return {
|
|
1792
1783
|
type: "image",
|
|
@@ -2030,6 +2021,7 @@ var deeplinkPlugin = definePlugin({
|
|
|
2030
2021
|
});
|
|
2031
2022
|
|
|
2032
2023
|
// src/plugins/ui-interact.ts
|
|
2024
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2033
2025
|
import { z as z12 } from "zod";
|
|
2034
2026
|
|
|
2035
2027
|
// src/utils/fiber.ts
|
|
@@ -2276,7 +2268,7 @@ var uiInteractPlugin = definePlugin({
|
|
|
2276
2268
|
let content = "";
|
|
2277
2269
|
try {
|
|
2278
2270
|
await ctx.exec(`adb shell uiautomator dump /sdcard/uidump.xml && adb pull /sdcard/uidump.xml ${tmpFile} 2>/dev/null`);
|
|
2279
|
-
content = await
|
|
2271
|
+
content = await readFile2(tmpFile, "utf8");
|
|
2280
2272
|
} finally {
|
|
2281
2273
|
await ctx.exec(`rm -f ${tmpFile}`).catch(() => {});
|
|
2282
2274
|
}
|
|
@@ -3103,7 +3095,7 @@ var commandsPlugin = definePlugin({
|
|
|
3103
3095
|
|
|
3104
3096
|
// src/plugins/test-recorder.ts
|
|
3105
3097
|
import { z as z16 } from "zod";
|
|
3106
|
-
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3098
|
+
import { readdir, readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
|
|
3107
3099
|
import { homedir } from "node:os";
|
|
3108
3100
|
import { join } from "node:path";
|
|
3109
3101
|
var CURRENT_ROUTE_JS = `
|
|
@@ -3565,7 +3557,7 @@ var testRecorderPlugin = definePlugin({
|
|
|
3565
3557
|
const filePath = join(RECORDINGS_DIR, `${safe}.json`);
|
|
3566
3558
|
let raw;
|
|
3567
3559
|
try {
|
|
3568
|
-
raw = await
|
|
3560
|
+
raw = await readFile3(filePath, "utf8");
|
|
3569
3561
|
} catch {
|
|
3570
3562
|
return `Recording not found: ${filePath}. Call list_test_recordings to see available files.`;
|
|
3571
3563
|
}
|
|
@@ -4894,7 +4886,12 @@ var statuslinePlugin = definePlugin({
|
|
|
4894
4886
|
name: "statusline",
|
|
4895
4887
|
description: "Writes CDP connection state to a file for Claude Code status bar integration",
|
|
4896
4888
|
async setup(ctx) {
|
|
4897
|
-
|
|
4889
|
+
let lastConnected = null;
|
|
4890
|
+
function write() {
|
|
4891
|
+
const connected = ctx.cdp.isConnected();
|
|
4892
|
+
if (connected === lastConnected)
|
|
4893
|
+
return;
|
|
4894
|
+
lastConnected = connected;
|
|
4898
4895
|
const target = ctx.cdp.getTarget();
|
|
4899
4896
|
writeStatus({
|
|
4900
4897
|
connected,
|
|
@@ -4904,9 +4901,10 @@ var statuslinePlugin = definePlugin({
|
|
|
4904
4901
|
updatedAt: Date.now()
|
|
4905
4902
|
});
|
|
4906
4903
|
}
|
|
4907
|
-
ctx.cdp.on("reconnected",
|
|
4908
|
-
ctx.cdp.on("disconnected",
|
|
4909
|
-
write
|
|
4904
|
+
ctx.cdp.on("reconnected", write);
|
|
4905
|
+
ctx.cdp.on("disconnected", write);
|
|
4906
|
+
setInterval(write, 5000);
|
|
4907
|
+
write();
|
|
4910
4908
|
ctx.registerTool("setup_statusline", {
|
|
4911
4909
|
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.",
|
|
4912
4910
|
parameters: z19.object({}),
|
|
@@ -4926,6 +4924,7 @@ var statuslinePlugin = definePlugin({
|
|
|
4926
4924
|
});
|
|
4927
4925
|
|
|
4928
4926
|
// src/server.ts
|
|
4927
|
+
var execAsync = promisify(exec);
|
|
4929
4928
|
var logger6 = createLogger("server");
|
|
4930
4929
|
var BUILT_IN_PLUGINS = [
|
|
4931
4930
|
consolePlugin,
|
|
@@ -5097,16 +5096,7 @@ async function startServer(config) {
|
|
|
5097
5096
|
}
|
|
5098
5097
|
},
|
|
5099
5098
|
exec: async (command) => {
|
|
5100
|
-
const
|
|
5101
|
-
stdout: "pipe",
|
|
5102
|
-
stderr: "pipe"
|
|
5103
|
-
});
|
|
5104
|
-
const stdout = await new Response(proc.stdout).text();
|
|
5105
|
-
const stderr = await new Response(proc.stderr).text();
|
|
5106
|
-
await proc.exited;
|
|
5107
|
-
if (proc.exitCode !== 0) {
|
|
5108
|
-
throw new Error(stderr || `Command failed with exit code ${proc.exitCode}`);
|
|
5109
|
-
}
|
|
5099
|
+
const { stdout } = await execAsync(command);
|
|
5110
5100
|
return stdout;
|
|
5111
5101
|
},
|
|
5112
5102
|
format: formatUtils
|
|
@@ -5195,28 +5185,13 @@ async function startServer(config) {
|
|
|
5195
5185
|
connectToMetro();
|
|
5196
5186
|
}
|
|
5197
5187
|
|
|
5198
|
-
// src/utils/logger.ts
|
|
5199
|
-
function createLogger2(name) {
|
|
5200
|
-
const prefix = `[metro-mcp:${name}]`;
|
|
5201
|
-
return {
|
|
5202
|
-
info: (msg, ...args) => console.error(prefix, msg, ...args),
|
|
5203
|
-
warn: (msg, ...args) => console.error(prefix, "WARN:", msg, ...args),
|
|
5204
|
-
error: (msg, ...args) => console.error(prefix, "ERROR:", msg, ...args),
|
|
5205
|
-
debug: (msg, ...args) => {
|
|
5206
|
-
if (process.env.DEBUG) {
|
|
5207
|
-
console.error(prefix, "DEBUG:", msg, ...args);
|
|
5208
|
-
}
|
|
5209
|
-
}
|
|
5210
|
-
};
|
|
5211
|
-
}
|
|
5212
|
-
|
|
5213
5188
|
// src/index.ts
|
|
5214
|
-
var logger7 =
|
|
5189
|
+
var logger7 = createLogger("main");
|
|
5215
5190
|
async function main() {
|
|
5216
5191
|
const args = process.argv.slice(2);
|
|
5217
5192
|
if (args.includes("--help") || args.includes("-h")) {
|
|
5218
5193
|
console.error(`
|
|
5219
|
-
metro-mcp
|
|
5194
|
+
metro-mcp — React Native MCP Server
|
|
5220
5195
|
|
|
5221
5196
|
Usage:
|
|
5222
5197
|
metro-mcp [options]
|
|
@@ -19,7 +19,7 @@ export declare class CDPClient implements CDPConnection {
|
|
|
19
19
|
private suppressReconnect;
|
|
20
20
|
private _isConnected;
|
|
21
21
|
private target;
|
|
22
|
-
private
|
|
22
|
+
private lastPingAt;
|
|
23
23
|
private readonly requestTimeout;
|
|
24
24
|
private readonly keepAliveInterval;
|
|
25
25
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/metro/connection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAA2B,WAAW,EAAE,MAAM,YAAY,CAAC;AAMvE,KAAK,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAQjE;;;;;;;GAOG;AACH,qBAAa,SAAU,YAAW,aAAa;IAC7C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/metro/connection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAA2B,WAAW,EAAE,MAAM,YAAY,CAAC;AAMvE,KAAK,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAQjE;;;;;;;GAOG;AACH,qBAAa,SAAU,YAAW,aAAa;IAC7C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAU3C,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAQ3C,OAAO,CAAC,SAAS;IAwDjB;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB9E;;OAEG;IACH,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAOjD;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAIlD;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,SAAS,IAAI,WAAW,GAAG,IAAI;IAI/B;;OAEG;IACH,UAAU,IAAI,IAAI;IAWlB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,aAAa;IAuCrB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,IAAI;CAYb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simulator.d.ts","sourceRoot":"","sources":["../../src/plugins/simulator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"simulator.d.ts","sourceRoot":"","sources":["../../src/plugins/simulator.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,eAAe,yCA0N1B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../../src/plugins/statusline.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,WAAW,+BAA+B,CAAC;AA4CxD,eAAO,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../../src/plugins/statusline.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,WAAW,+BAA+B,CAAC;AA4CxD,eAAO,MAAM,gBAAgB,yCAiD3B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-interact.d.ts","sourceRoot":"","sources":["../../src/plugins/ui-interact.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ui-interact.d.ts","sourceRoot":"","sources":["../../src/plugins/ui-interact.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,gBAAgB,yCAuiB3B,CAAC"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,aAAa,CAAC;AAwDrB,wBAAsB,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA4RjF"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Plugin-based MCP server for React Native/Expo runtime debugging, inspection, and automation via Metro/CDP",
|
|
5
5
|
"homepage": "https://github.com/steve228uk/metro-mcp",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/steve228uk/metro-mcp.git"
|
|
8
|
+
"url": "git+https://github.com/steve228uk/metro-mcp.git"
|
|
9
9
|
},
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/steve228uk/metro-mcp/issues"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"clean": "rm -rf dist",
|
|
42
42
|
"build": "bun run clean && bun run build:js && bun run build:bin && bun run build:types",
|
|
43
43
|
"build:js": "bun build src/index.ts src/plugin.ts --outdir dist --target node --external @modelcontextprotocol/sdk --external zod --external ws && bun build src/client/index.ts --outfile dist/client/index.js --target browser --external @modelcontextprotocol/sdk --external zod && bun build src/client/index.ts --outfile dist/client/index.cjs --target browser --format cjs --external @modelcontextprotocol/sdk --external zod && bun build src/plugin.ts --outfile dist/plugin.cjs --target node --format cjs --external @modelcontextprotocol/sdk --external zod",
|
|
44
|
-
"build:bin": "bun build bin/metro-mcp.ts --outfile dist/bin/metro-mcp.js --target
|
|
44
|
+
"build:bin": "bun build bin/metro-mcp.ts --outfile dist/bin/metro-mcp.js --target node --external @modelcontextprotocol/sdk --external zod --external ws && chmod +x dist/bin/metro-mcp.js",
|
|
45
45
|
"build:types": "bunx tsc -p tsconfig.build.json",
|
|
46
46
|
"typecheck": "tsc --noEmit"
|
|
47
47
|
},
|