@ohah/react-native-mcp-server 0.1.0-rc.1

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/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @ohah/react-native-mcp-server
2
+
3
+ > [한국어 문서](./README_KO.md)
4
+
5
+ MCP (Model Context Protocol) server for React Native app automation and monitoring. Lets AI assistants (Cursor, Claude Desktop, GitHub Copilot CLI) inspect and control your React Native app over WebSocket.
6
+
7
+ ## Features
8
+
9
+ - Monitor app state, network requests, and console logs
10
+ - Tap, swipe, type text, take screenshots via idb (iOS) / adb (Android)
11
+ - Query the component tree, run scripts in the app context
12
+ - One-command setup with `init`
13
+
14
+ ## Install
15
+
16
+ **Prerequisites:** Node.js 18+ or Bun. For tap/swipe/screenshots you need [idb](https://fbidb.io/) (iOS, macOS) and [adb](https://developer.android.com/tools/adb) (Android).
17
+
18
+ No global install required — use with npx:
19
+
20
+ ```bash
21
+ npx -y @ohah/react-native-mcp-server init
22
+ ```
23
+
24
+ This detects your project (React Native / Expo, package manager), asks for your MCP client (Cursor, Claude, etc.), and configures Babel + MCP config + .gitignore.
25
+
26
+ Or install globally:
27
+
28
+ ```bash
29
+ npm install -g @ohah/react-native-mcp-server
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ 1. **Setup** (once per project):
35
+
36
+ ```bash
37
+ npx -y @ohah/react-native-mcp-server init
38
+ ```
39
+
40
+ 2. **Start your app.** In development, the MCP runtime is included automatically (no extra env needed). Example:
41
+
42
+ ```bash
43
+ npx react-native start
44
+ # or for Expo: npx expo start
45
+ ```
46
+
47
+ To enable MCP in release builds, run Metro with `REACT_NATIVE_MCP_ENABLED=true` or set it in your build config.
48
+
49
+ 3. **Configure your MCP client** (e.g. Cursor → Settings → MCP). The init command creates the config; typical entry:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "react-native-mcp": {
55
+ "command": "npx",
56
+ "args": ["-y", "@ohah/react-native-mcp-server"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ 4. Open your app and Cursor (or Claude/Copilot); the server connects to the app automatically.
63
+
64
+ ## Init options
65
+
66
+ ```bash
67
+ npx -y @ohah/react-native-mcp-server init -y # Non-interactive (Cursor default)
68
+ npx -y @ohah/react-native-mcp-server init --client cursor # Cursor
69
+ npx -y @ohah/react-native-mcp-server init --client claude-code # Claude Code (CLI)
70
+ npx -y @ohah/react-native-mcp-server init --client claude-desktop
71
+ npx -y @ohah/react-native-mcp-server init --help
72
+ ```
73
+
74
+ ## Manual Babel setup
75
+
76
+ If you prefer not to use `init`, add the Babel preset yourself so the app can connect to the MCP server:
77
+
78
+ ```js
79
+ // babel.config.js
80
+ module.exports = {
81
+ presets: [
82
+ 'module:@react-native/babel-preset', // or your existing preset
83
+ '@ohah/react-native-mcp-server/babel-preset',
84
+ ],
85
+ };
86
+ ```
87
+
88
+ Then add the MCP server entry to your client config (e.g. `.cursor/mcp.json` or Claude config) as in the Quick Start step 3.
89
+
90
+ ## Native tools (idb / adb)
91
+
92
+ For tap, swipe, screenshots, and similar tools you need:
93
+
94
+ | Platform | Tool | Install |
95
+ | ------------- | ---- | ------------------------------------------------------------------------------------------ |
96
+ | iOS Simulator | idb | `brew tap facebook/fb && brew install idb-companion` (macOS) |
97
+ | Android | adb | Install Android Studio (adb is included), or `brew install android-platform-tools` (macOS) |
98
+
99
+ Without these, other MCP tools (state, network, console, eval) still work.
100
+
101
+ ## Documentation
102
+
103
+ - **Full docs:** [https://ohah.github.io/react-native-mcp/](https://ohah.github.io/react-native-mcp/)
104
+ - **Repository:** [github.com/ohah/react-native-mcp](https://github.com/ohah/react-native-mcp)
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,36 @@
1
+
2
+ //#region src/babel-plugin-app-registry.ts
3
+ const MCP_RUNTIME_ID = "__REACT_NATIVE_MCP__";
4
+ const RUNTIME_MODULE_ID = "@ohah/react-native-mcp-server/runtime";
5
+ function babel_plugin_app_registry_default(babel) {
6
+ const t = babel.types;
7
+ return {
8
+ name: "react-native-mcp-app-registry",
9
+ visitor: {
10
+ Program: { enter(_path, state) {
11
+ state.runtimeInjected = false;
12
+ } },
13
+ CallExpression(path, state) {
14
+ const node = path.node;
15
+ if ((path.hub?.file?.opts?.filename ?? "").includes("node_modules")) return;
16
+ if (!t.isCallExpression(node)) return;
17
+ if (!t.isMemberExpression(node.callee)) return;
18
+ const { object, property } = node.callee;
19
+ if (!t.isIdentifier(object) || !t.isIdentifier(property)) return;
20
+ if (object.name !== "AppRegistry" || property.name !== "registerComponent") return;
21
+ if (!state.runtimeInjected) {
22
+ const programPath = path.findParent((p) => p.isProgram?.());
23
+ if (programPath?.node?.body) {
24
+ programPath.node.body.unshift(t.expressionStatement(t.callExpression(t.identifier("require"), [t.stringLiteral(RUNTIME_MODULE_ID)])));
25
+ programPath.node.body.unshift(t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.identifier("global"), t.identifier("__REACT_NATIVE_MCP_ENABLED__")), t.booleanLiteral(true))));
26
+ state.runtimeInjected = true;
27
+ }
28
+ }
29
+ path.replaceWith(t.callExpression(t.memberExpression(t.identifier(MCP_RUNTIME_ID), t.identifier("registerComponent")), node.arguments));
30
+ }
31
+ }
32
+ };
33
+ }
34
+
35
+ //#endregion
36
+ module.exports = babel_plugin_app_registry_default;
@@ -0,0 +1,115 @@
1
+
2
+ //#region src/babel-plugin-inject-testid.ts
3
+ function getTestIdStringLiteral(t, attr) {
4
+ if (!attr.value) return null;
5
+ if (t.isStringLiteral(attr.value)) return attr.value.value;
6
+ if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)) return attr.value.expression.value;
7
+ return null;
8
+ }
9
+ function getTagName(t, name) {
10
+ if (t.isJSXIdentifier(name)) return name.name;
11
+ if (t.isJSXNamespacedName(name)) return `${name.namespace.name}.${name.name.name}`;
12
+ const parts = [];
13
+ let cur = name;
14
+ while (true) {
15
+ if (t.isJSXIdentifier(cur.object)) parts.push(cur.object.name);
16
+ else parts.push(getTagName(t, cur.object));
17
+ if (t.isJSXIdentifier(cur.property)) {
18
+ parts.push(cur.property.name);
19
+ break;
20
+ }
21
+ cur = cur.property;
22
+ }
23
+ return parts.join(".");
24
+ }
25
+ function babel_plugin_inject_testid_default(babel) {
26
+ const t = babel.types;
27
+ return {
28
+ name: "react-native-mcp-inject-testid",
29
+ visitor: {
30
+ Program: { enter(_path, state) {
31
+ state.stack = [];
32
+ } },
33
+ Function: {
34
+ enter(path, state) {
35
+ let name = t.isFunctionDeclaration(path.node) || t.isFunctionExpression(path.node) ? path.node.id?.name ?? null : null;
36
+ if (!name && path.parent && t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) name = path.parent.id.name;
37
+ if (!name && t.isIdentifier(path.node.params[0])) name = path.node.params[0].name;
38
+ state.stack.push({
39
+ componentName: name ?? "Anonymous",
40
+ jsxIndex: 0
41
+ });
42
+ },
43
+ exit(path, state) {
44
+ const scope = state.stack[state.stack.length - 1];
45
+ if (scope && scope.jsxIndex > 0 && scope.componentName !== "Anonymous" && /^[A-Z]/.test(scope.componentName)) {
46
+ const stmt = t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.identifier(scope.componentName), t.identifier("displayName")), t.stringLiteral(scope.componentName)));
47
+ let target = null;
48
+ if (t.isFunctionDeclaration(path.node)) target = path.parentPath && (t.isExportDefaultDeclaration(path.parent) || t.isExportNamedDeclaration(path.parent)) ? path.parentPath : path;
49
+ else if (path.parentPath && t.isVariableDeclarator(path.parent)) {
50
+ target = path.parentPath.parentPath;
51
+ if (target?.parentPath && (t.isExportDefaultDeclaration(target.parent) || t.isExportNamedDeclaration(target.parent))) target = target.parentPath;
52
+ }
53
+ target?.insertAfter(stmt);
54
+ }
55
+ state.stack.pop();
56
+ }
57
+ },
58
+ JSXOpeningElement(path, state) {
59
+ if ((path.hub?.file?.opts?.filename ?? "").includes("node_modules")) return;
60
+ const scope = state.stack[state.stack.length - 1];
61
+ if (scope === void 0) return;
62
+ const el = path.node;
63
+ const testIdAttr = el.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "testID");
64
+ if (!!!testIdAttr) {
65
+ const tagName = getTagName(t, el.name);
66
+ const baseValue = `${scope.componentName}-${scope.jsxIndex}-${tagName}`;
67
+ const keyAttr = el.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "key");
68
+ let keyExpr = null;
69
+ if (keyAttr?.value) {
70
+ if (t.isStringLiteral(keyAttr.value)) keyExpr = keyAttr.value;
71
+ else if (t.isJSXExpressionContainer(keyAttr.value) && !t.isJSXEmptyExpression(keyAttr.value.expression)) keyExpr = keyAttr.value.expression;
72
+ }
73
+ if (keyExpr) {
74
+ const tpl = t.templateLiteral([t.templateElement({
75
+ raw: baseValue + "-",
76
+ cooked: baseValue + "-"
77
+ }, false), t.templateElement({
78
+ raw: "",
79
+ cooked: ""
80
+ }, true)], [t.cloneNode(keyExpr)]);
81
+ el.attributes.push(t.jsxAttribute(t.jsxIdentifier("testID"), t.jsxExpressionContainer(tpl)));
82
+ } else el.attributes.push(t.jsxAttribute(t.jsxIdentifier("testID"), t.stringLiteral(baseValue)));
83
+ scope.jsxIndex += 1;
84
+ }
85
+ if (getTagName(t, el.name) === "WebView") {
86
+ const webViewTidAttr = testIdAttr ?? el.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "testID");
87
+ const webViewTestIdValue = webViewTidAttr ? getTestIdStringLiteral(t, webViewTidAttr) : null;
88
+ if (webViewTestIdValue != null) {
89
+ const refAttr = el.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "ref");
90
+ const mcpRegister = t.expressionStatement(t.logicalExpression("&&", t.optionalMemberExpression(t.identifier("__REACT_NATIVE_MCP__"), t.identifier("registerWebView"), false, true), t.callExpression(t.memberExpression(t.optionalMemberExpression(t.identifier("__REACT_NATIVE_MCP__"), t.identifier("registerWebView"), false, true), t.identifier("call"), false), [
91
+ t.identifier("__REACT_NATIVE_MCP__"),
92
+ t.identifier("r"),
93
+ t.stringLiteral(webViewTestIdValue)
94
+ ])));
95
+ const mcpUnregister = t.expressionStatement(t.logicalExpression("&&", t.optionalMemberExpression(t.identifier("__REACT_NATIVE_MCP__"), t.identifier("unregisterWebView"), false, true), t.callExpression(t.memberExpression(t.optionalMemberExpression(t.identifier("__REACT_NATIVE_MCP__"), t.identifier("unregisterWebView"), false, true), t.identifier("call"), false), [t.identifier("__REACT_NATIVE_MCP__"), t.stringLiteral(webViewTestIdValue)])));
96
+ const bodyStatements = [t.ifStatement(t.identifier("r"), mcpRegister, mcpUnregister)];
97
+ const userRefExpr = refAttr?.value && t.isJSXExpressionContainer(refAttr.value) ? refAttr.value.expression : null;
98
+ if (userRefExpr != null && !t.isJSXEmptyExpression(userRefExpr)) bodyStatements.push(t.ifStatement(t.binaryExpression("!=", t.cloneNode(userRefExpr), t.nullLiteral()), t.blockStatement([t.ifStatement(t.binaryExpression("===", t.unaryExpression("typeof", t.cloneNode(userRefExpr)), t.stringLiteral("function")), t.expressionStatement(t.callExpression(t.cloneNode(userRefExpr), [t.identifier("r")])), t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.cloneNode(userRefExpr), t.identifier("current")), t.identifier("r"))))])));
99
+ const composedRef = t.arrowFunctionExpression([t.identifier("r")], t.blockStatement(bodyStatements));
100
+ if (refAttr) refAttr.value = t.jsxExpressionContainer(composedRef);
101
+ else el.attributes.push(t.jsxAttribute(t.jsxIdentifier("ref"), t.jsxExpressionContainer(composedRef)));
102
+ const onMessageAttr = el.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "onMessage");
103
+ const userOnMessageExpr = onMessageAttr?.value && t.isJSXExpressionContainer(onMessageAttr.value) ? onMessageAttr.value.expression : null;
104
+ const mcpOnMessage = userOnMessageExpr != null && !t.isJSXEmptyExpression(userOnMessageExpr) ? t.callExpression(t.memberExpression(t.identifier("__REACT_NATIVE_MCP__"), t.identifier("createWebViewOnMessage"), false), [userOnMessageExpr]) : t.arrowFunctionExpression([t.identifier("e")], t.callExpression(t.memberExpression(t.identifier("__REACT_NATIVE_MCP__"), t.identifier("handleWebViewMessage"), false), [t.memberExpression(t.memberExpression(t.identifier("e"), t.identifier("nativeEvent")), t.identifier("data"))]));
105
+ if (onMessageAttr) onMessageAttr.value = t.jsxExpressionContainer(mcpOnMessage);
106
+ else el.attributes.push(t.jsxAttribute(t.jsxIdentifier("onMessage"), t.jsxExpressionContainer(mcpOnMessage)));
107
+ }
108
+ }
109
+ }
110
+ }
111
+ };
112
+ }
113
+
114
+ //#endregion
115
+ module.exports = babel_plugin_inject_testid_default;
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Babel preset: AppRegistry 래핑 + testID 자동 주입
5
+ * babel.config.js에서 presets에 한 번만 넣으면 됨.
6
+ * - __DEV__ 빌드: 자동 활성화 (환경변수 불필요)
7
+ * - Release 빌드: REACT_NATIVE_MCP_ENABLED=true|1 일 때만 활성화
8
+ */
9
+ function isMcpEnabled() {
10
+ // Metro는 DEV 빌드 시 NODE_ENV를 'development'로 설정한다
11
+ if (process.env.NODE_ENV !== 'production') return true;
12
+ const v = process.env.REACT_NATIVE_MCP_ENABLED;
13
+ return v === 'true' || v === '1';
14
+ }
15
+
16
+ module.exports = function () {
17
+ if (!isMcpEnabled()) {
18
+ return { plugins: [] };
19
+ }
20
+ return {
21
+ plugins: [
22
+ require('./babel-plugin-app-registry.cjs'),
23
+ require('./babel-plugin-inject-testid.cjs'),
24
+ ],
25
+ };
26
+ };
@@ -0,0 +1,206 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ //#region src/tools/run-command.ts
4
+ /**
5
+ * 공유 CLI 명령 실행 유틸리티
6
+ * take-screenshot, idb 도구 등 호스트 CLI를 실행하는 모든 도구에서 사용.
7
+ */
8
+ function runCommand(command, args, options) {
9
+ return new Promise((resolve, reject) => {
10
+ const proc = spawn(command, args, { stdio: options?.stdin ? [
11
+ "pipe",
12
+ "pipe",
13
+ "pipe"
14
+ ] : [
15
+ "ignore",
16
+ "pipe",
17
+ "pipe"
18
+ ] });
19
+ const outChunks = [];
20
+ const errChunks = [];
21
+ proc.stdout?.on("data", (chunk) => outChunks.push(chunk));
22
+ proc.stderr?.on("data", (chunk) => errChunks.push(chunk));
23
+ const timeout = options?.timeoutMs != null ? setTimeout(() => {
24
+ proc.kill("SIGKILL");
25
+ reject(/* @__PURE__ */ new Error("Command timed out"));
26
+ }, options.timeoutMs) : void 0;
27
+ proc.on("close", (code) => {
28
+ clearTimeout(timeout);
29
+ if (code !== 0) {
30
+ const stderr = Buffer.concat(errChunks).toString("utf8").slice(0, 300);
31
+ reject(/* @__PURE__ */ new Error(`Command failed with code ${code}. ${stderr}`));
32
+ return;
33
+ }
34
+ resolve(Buffer.concat(outChunks));
35
+ });
36
+ proc.on("error", (err) => {
37
+ clearTimeout(timeout);
38
+ reject(err);
39
+ });
40
+ if (options?.stdin && proc.stdin) {
41
+ proc.stdin.write(options.stdin);
42
+ proc.stdin.end();
43
+ }
44
+ });
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/tools/adb-utils.ts
49
+ /**
50
+ * adb (Android Debug Bridge) 공유 유틸리티
51
+ * 디바이스 시리얼 자동 해석, adb 설치 확인, 명령 실행 래퍼.
52
+ */
53
+ let _adbAvailable = null;
54
+ async function checkAdbAvailable() {
55
+ if (_adbAvailable != null) return _adbAvailable;
56
+ try {
57
+ await runCommand("which", ["adb"], { timeoutMs: 3e3 });
58
+ _adbAvailable = true;
59
+ } catch {
60
+ _adbAvailable = false;
61
+ }
62
+ return _adbAvailable;
63
+ }
64
+ async function listAdbDevices() {
65
+ const lines = (await runCommand("adb", ["devices", "-l"], { timeoutMs: 1e4 })).toString("utf8").split("\n").filter((l) => l.trim().length > 0);
66
+ const devices = [];
67
+ for (const line of lines) {
68
+ if (line.startsWith("List of")) continue;
69
+ const match = line.match(/^(\S+)\s+(device|offline|unauthorized|no permissions)\b(.*)$/);
70
+ if (!match) continue;
71
+ const serial = match[1];
72
+ const state = match[2];
73
+ const rest = match[3] ?? "";
74
+ const device = {
75
+ serial,
76
+ state
77
+ };
78
+ const productMatch = rest.match(/product:(\S+)/);
79
+ if (productMatch) device.product = productMatch[1];
80
+ const modelMatch = rest.match(/model:(\S+)/);
81
+ if (modelMatch) device.model = modelMatch[1];
82
+ const deviceMatch = rest.match(/device:(\S+)/);
83
+ if (deviceMatch) device.device = deviceMatch[1];
84
+ const transportMatch = rest.match(/transport_id:(\S+)/);
85
+ if (transportMatch) device.transport_id = transportMatch[1];
86
+ devices.push(device);
87
+ }
88
+ return devices;
89
+ }
90
+ async function resolveSerial(serial) {
91
+ if (serial != null && serial !== "") return serial;
92
+ const online = (await listAdbDevices()).filter((d) => d.state === "device");
93
+ if (online.length === 0) throw new Error("No connected Android device found. Connect a device or start an emulator with: emulator -avd <avd_name>");
94
+ if (online.length > 1) {
95
+ const list = online.map((d) => ` ${d.serial}${d.model ? ` (${d.model})` : ""}`).join("\n");
96
+ throw new Error(`Multiple Android devices connected. Specify serial parameter.\n${list}\nUse list_devices(platform="android") to see all devices.`);
97
+ }
98
+ return online[0].serial;
99
+ }
100
+ /**
101
+ * 해당 Android 대상이 에뮬레이터(AVD)인지 여부.
102
+ * - adb shell getprop ro.kernel.qemu → "1" 이면 에뮬.
103
+ * - 시리얼이 "emulator-" 로 시작해도 에뮬로 간주 (getprop 실패 시 fallback).
104
+ */
105
+ async function isAndroidEmulator(serial) {
106
+ try {
107
+ return (await runAdbCommand([
108
+ "shell",
109
+ "getprop",
110
+ "ro.kernel.qemu"
111
+ ], serial, { timeoutMs: 3e3 })).trim() === "1";
112
+ } catch {
113
+ return serial.startsWith("emulator-");
114
+ }
115
+ }
116
+ async function runAdbCommand(subcommand, serial, options) {
117
+ const args = [];
118
+ if (serial) args.push("-s", serial);
119
+ args.push(...subcommand);
120
+ return (await runCommand("adb", args, { timeoutMs: options?.timeoutMs ?? 1e4 })).toString("utf8").trim();
121
+ }
122
+ const _scaleBySerial = /* @__PURE__ */ new Map();
123
+ /**
124
+ * Android screen density scale (디바이스별 캐싱).
125
+ * density 160 = 1x, 320 = 2x, 480 = 3x 등.
126
+ * dp × scale = pixel.
127
+ */
128
+ async function getAndroidScale(serial) {
129
+ const key = serial ?? "_default";
130
+ const cached = _scaleBySerial.get(key);
131
+ if (cached != null) return cached;
132
+ try {
133
+ const match = (await runCommand("adb", serial ? [
134
+ "-s",
135
+ serial,
136
+ "shell",
137
+ "wm",
138
+ "density"
139
+ ] : [
140
+ "shell",
141
+ "wm",
142
+ "density"
143
+ ], { timeoutMs: 5e3 })).toString().match(/(\d+)/);
144
+ if (!match) throw new Error("density not found in wm output");
145
+ const scale = parseInt(match[1], 10) / 160;
146
+ _scaleBySerial.set(key, scale);
147
+ return scale;
148
+ } catch {
149
+ return 2.75;
150
+ }
151
+ }
152
+ const _topInsetBySerial = /* @__PURE__ */ new Map();
153
+ /**
154
+ * Android 디바이스의 실제 top inset(px)을 `dumpsys window displays`에서 파싱.
155
+ * captionBar(태블릿 등)가 있으면 우선, 없으면 statusBars 사용.
156
+ * 파싱 실패 시 0 반환 (호출자가 fallback 처리).
157
+ */
158
+ async function getAndroidTopInset(serial) {
159
+ const key = serial ?? "_default";
160
+ const cached = _topInsetBySerial.get(key);
161
+ if (cached != null) return cached;
162
+ try {
163
+ const text = await runAdbCommand([
164
+ "shell",
165
+ "dumpsys",
166
+ "window",
167
+ "displays"
168
+ ], serial, { timeoutMs: 5e3 });
169
+ const captionMatch = text.match(/InsetsSource[^\n]*type=captionBar[^\n]*frame=\[\d+,\d+\]\[\d+,(\d+)\]/);
170
+ if (captionMatch) {
171
+ const px = parseInt(captionMatch[1], 10);
172
+ _topInsetBySerial.set(key, px);
173
+ return px;
174
+ }
175
+ const statusMatch = text.match(/InsetsSource[^\n]*type=statusBars[^\n]*frame=\[\d+,\d+\]\[\d+,(\d+)\]/);
176
+ if (statusMatch) {
177
+ const px = parseInt(statusMatch[1], 10);
178
+ _topInsetBySerial.set(key, px);
179
+ return px;
180
+ }
181
+ _topInsetBySerial.set(key, 0);
182
+ return 0;
183
+ } catch {
184
+ return 0;
185
+ }
186
+ }
187
+ function adbNotInstalledError() {
188
+ return {
189
+ isError: true,
190
+ content: [{
191
+ type: "text",
192
+ text: [
193
+ "adb (Android Debug Bridge) is not installed or not in PATH.",
194
+ "",
195
+ "Install Android SDK Platform-Tools:",
196
+ " macOS: brew install android-platform-tools",
197
+ " Or download from: https://developer.android.com/tools/releases/platform-tools",
198
+ "",
199
+ "Verify: adb devices"
200
+ ].join("\n")
201
+ }]
202
+ };
203
+ }
204
+
205
+ //#endregion
206
+ export { isAndroidEmulator as a, runAdbCommand as c, getAndroidTopInset as i, runCommand as l, checkAdbAvailable as n, listAdbDevices as o, getAndroidScale as r, resolveSerial as s, adbNotInstalledError as t };
@@ -0,0 +1,3 @@
1
+ import { a as isAndroidEmulator, c as runAdbCommand, i as getAndroidTopInset, n as checkAdbAvailable, o as listAdbDevices, r as getAndroidScale, s as resolveSerial, t as adbNotInstalledError } from "./adb-utils-DreOsWp-.js";
2
+
3
+ export { getAndroidScale };
@@ -0,0 +1,10 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * React Native MCP 서버 진입점
4
+ * Stdio transport (Cursor 연동) + WebSocket 서버 (앱 연동)
5
+ *
6
+ * `init` 서브커맨드: 프로젝트 셋업 CLI
7
+ */
8
+ declare const VERSION = "0.1.0";
9
+ //#endregion
10
+ export { VERSION };