@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.
@@ -0,0 +1,330 @@
1
+ import { execSync } from "node:child_process";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import readline from "node:readline/promises";
6
+ import { stdin, stdout } from "node:process";
7
+
8
+ //#region src/init/detect.ts
9
+ /**
10
+ * 프로젝트 타입 감지 (Expo vs bare RN, 패키지 매니저, babel 설정 등)
11
+ */
12
+ const BABEL_CONFIG_FILES = [
13
+ "babel.config.js",
14
+ "babel.config.cjs",
15
+ "babel.config.mjs",
16
+ ".babelrc",
17
+ ".babelrc.js"
18
+ ];
19
+ function detectProject(cwd) {
20
+ const pkgPath = path.join(cwd, "package.json");
21
+ let rnVersion = null;
22
+ let expoVersion = null;
23
+ if (fs.existsSync(pkgPath)) try {
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
25
+ const deps = {
26
+ ...pkg.dependencies,
27
+ ...pkg.devDependencies
28
+ };
29
+ rnVersion = deps["react-native"] ?? null;
30
+ expoVersion = deps["expo"] ?? null;
31
+ } catch {}
32
+ const isExpo = expoVersion != null || fs.existsSync(path.join(cwd, "app.json")) || fs.existsSync(path.join(cwd, "app.config.js")) || fs.existsSync(path.join(cwd, "app.config.ts"));
33
+ let babelConfigPath = null;
34
+ for (const name of BABEL_CONFIG_FILES) {
35
+ const full = path.join(cwd, name);
36
+ if (fs.existsSync(full)) {
37
+ babelConfigPath = full;
38
+ break;
39
+ }
40
+ }
41
+ return {
42
+ isExpo,
43
+ rnVersion,
44
+ expoVersion,
45
+ hasBabelConfig: babelConfigPath != null,
46
+ babelConfigPath,
47
+ packageManager: detectPackageManager(cwd)
48
+ };
49
+ }
50
+ function detectPackageManager(cwd) {
51
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
52
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
53
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
54
+ return "npm";
55
+ }
56
+ function checkCommand(cmd, versionFlag = "--version") {
57
+ try {
58
+ return {
59
+ installed: true,
60
+ version: execSync(`${cmd} ${versionFlag}`, {
61
+ stdio: [
62
+ "pipe",
63
+ "pipe",
64
+ "pipe"
65
+ ],
66
+ timeout: 5e3
67
+ }).toString().trim().split("\n")[0]
68
+ };
69
+ } catch {
70
+ return {
71
+ installed: false,
72
+ version: null
73
+ };
74
+ }
75
+ }
76
+ function checkExternalTools() {
77
+ const isMac = process.platform === "darwin";
78
+ const tools = [];
79
+ const docsBase = "https://ohah.github.io/react-native-mcp/mcp/#4-native-tools-idb--adb";
80
+ const adb = checkCommand("adb");
81
+ tools.push({
82
+ name: "adb",
83
+ installed: adb.installed,
84
+ version: adb.version,
85
+ hint: isMac ? `brew install android-platform-tools or install Android Studio\n Docs: ${docsBase}` : `Install Android Studio (includes adb) or sudo apt install adb\n Docs: ${docsBase}`
86
+ });
87
+ if (isMac) {
88
+ const idb = checkCommand("idb");
89
+ tools.push({
90
+ name: "idb",
91
+ installed: idb.installed,
92
+ version: idb.version,
93
+ hint: `brew tap facebook/fb && brew install idb-companion\n Docs: ${docsBase}`
94
+ });
95
+ }
96
+ return tools;
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/init/babel-config.ts
101
+ /**
102
+ * babel.config.js에 MCP babel preset 추가
103
+ */
104
+ const MCP_PRESET = "@ohah/react-native-mcp-server/babel-preset";
105
+ function updateBabelConfig(info) {
106
+ if (!info.hasBabelConfig || !info.babelConfigPath) return {
107
+ success: false,
108
+ skipped: false,
109
+ message: "babel.config.js not found — add the preset manually"
110
+ };
111
+ const content = fs.readFileSync(info.babelConfigPath, "utf-8");
112
+ if (content.includes(MCP_PRESET)) return {
113
+ success: true,
114
+ skipped: true,
115
+ message: "preset already configured"
116
+ };
117
+ const match = content.match(/(presets\s*:\s*\[)([\s\S]*?)(\])/);
118
+ if (!match) return {
119
+ success: false,
120
+ skipped: false,
121
+ message: `Could not find presets array — add manually:\n presets: [..., '${MCP_PRESET}']`
122
+ };
123
+ const fullMatch = match[0];
124
+ const before = match[1];
125
+ const presetsContent = match[2];
126
+ const after = match[3];
127
+ if (before === void 0 || presetsContent === void 0 || after === void 0) return {
128
+ success: false,
129
+ skipped: false,
130
+ message: `Could not find presets array — add manually:\n presets: [..., '${MCP_PRESET}']`
131
+ };
132
+ const trimmed = presetsContent.trimEnd();
133
+ const separator = trimmed.length > 0 && !trimmed.endsWith(",") ? "," : "";
134
+ const newContent = content.replace(fullMatch, `${before}${presetsContent.trimEnd()}${separator} '${MCP_PRESET}'${after}`);
135
+ fs.writeFileSync(info.babelConfigPath, newContent, "utf-8");
136
+ return {
137
+ success: true,
138
+ skipped: false,
139
+ message: "preset added"
140
+ };
141
+ }
142
+
143
+ //#endregion
144
+ //#region src/init/mcp-config.ts
145
+ /**
146
+ * MCP 클라이언트 설정 파일 생성/업데이트
147
+ */
148
+ const MCP_CLIENTS = [
149
+ {
150
+ label: "Cursor",
151
+ value: "cursor"
152
+ },
153
+ {
154
+ label: "Claude Code (CLI)",
155
+ value: "claude-code"
156
+ },
157
+ {
158
+ label: "Claude Desktop",
159
+ value: "claude-desktop"
160
+ },
161
+ {
162
+ label: "Windsurf",
163
+ value: "windsurf"
164
+ },
165
+ {
166
+ label: "Antigravity",
167
+ value: "antigravity"
168
+ }
169
+ ];
170
+ const MCP_SERVER_ENTRY = {
171
+ command: "npx",
172
+ args: ["-y", "@ohah/react-native-mcp-server"]
173
+ };
174
+ function setupMcpConfig(client, cwd) {
175
+ switch (client) {
176
+ case "cursor": return writeJsonConfig(path.join(cwd, ".cursor", "mcp.json"));
177
+ case "windsurf": return writeJsonConfig(path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"));
178
+ case "antigravity": return writeJsonConfig(path.join(os.homedir(), ".gemini", "antigravity", "mcp_config.json"));
179
+ case "claude-desktop": return writeClaudeDesktopConfig();
180
+ case "claude-code": return runClaudeCodeAdd();
181
+ }
182
+ }
183
+ function writeJsonConfig(configPath) {
184
+ const dir = path.dirname(configPath);
185
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
186
+ let existing = {};
187
+ if (fs.existsSync(configPath)) try {
188
+ existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
189
+ } catch {}
190
+ const mcpServers = existing.mcpServers ?? {};
191
+ if (mcpServers["react-native-mcp"]) return {
192
+ success: true,
193
+ message: "already configured"
194
+ };
195
+ mcpServers["react-native-mcp"] = MCP_SERVER_ENTRY;
196
+ existing.mcpServers = mcpServers;
197
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
198
+ return {
199
+ success: true,
200
+ message: `created ${path.relative(process.cwd(), configPath)}`
201
+ };
202
+ }
203
+ function getClaudeDesktopConfigPath() {
204
+ if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
205
+ if (process.platform === "win32") return path.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
206
+ return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
207
+ }
208
+ function writeClaudeDesktopConfig() {
209
+ return writeJsonConfig(getClaudeDesktopConfigPath());
210
+ }
211
+ function runClaudeCodeAdd() {
212
+ try {
213
+ execSync("claude mcp add react-native-mcp -- npx -y @ohah/react-native-mcp-server", { stdio: "pipe" });
214
+ return {
215
+ success: true,
216
+ message: "added via claude mcp add"
217
+ };
218
+ } catch (err) {
219
+ return {
220
+ success: false,
221
+ message: `claude mcp add failed: ${err instanceof Error ? err.message : String(err)}\nRun manually: claude mcp add react-native-mcp -- npx -y @ohah/react-native-mcp-server`
222
+ };
223
+ }
224
+ }
225
+
226
+ //#endregion
227
+ //#region src/init/prompts.ts
228
+ /**
229
+ * readline 기반 인터랙티브 프롬프트
230
+ */
231
+ let rl = null;
232
+ function getRL() {
233
+ if (!rl) rl = readline.createInterface({
234
+ input: stdin,
235
+ output: stdout
236
+ });
237
+ return rl;
238
+ }
239
+ function closeRL() {
240
+ if (rl) {
241
+ rl.close();
242
+ rl = null;
243
+ }
244
+ }
245
+ async function select(question, items, labelFn) {
246
+ const r = getRL();
247
+ console.log();
248
+ console.log(question);
249
+ for (const [i, item] of items.entries()) console.log(` ${i + 1}. ${labelFn(item)}`);
250
+ while (true) {
251
+ const answer = await r.question(`> `);
252
+ const num = parseInt(answer.trim(), 10);
253
+ if (num >= 1 && num <= items.length) {
254
+ const chosen = items[num - 1];
255
+ if (chosen !== void 0) return chosen;
256
+ }
257
+ console.log(` Please enter a number between 1 and ${items.length}`);
258
+ }
259
+ }
260
+
261
+ //#endregion
262
+ //#region src/init/index.ts
263
+ /**
264
+ * `npx react-native-mcp init` 메인 플로우
265
+ */
266
+ async function runInit(options = {}) {
267
+ const cwd = process.cwd();
268
+ const interactive = options.interactive !== false;
269
+ console.log();
270
+ console.log("\x1B[1m React Native MCP Setup\x1B[0m");
271
+ console.log();
272
+ console.log(" Detecting project...");
273
+ const info = detectProject(cwd);
274
+ if (!info.rnVersion) {
275
+ console.log(" \x1B[33m⚠\x1B[0m React Native not found in package.json");
276
+ console.log(" Make sure you are in a React Native project directory.");
277
+ process.exitCode = 1;
278
+ closeRL();
279
+ return;
280
+ }
281
+ console.log(` \x1b[32m✓\x1b[0m React Native ${info.rnVersion}`);
282
+ if (info.isExpo) console.log(` \x1b[32m✓\x1b[0m Expo detected${info.expoVersion ? ` (expo@${info.expoVersion})` : ""}`);
283
+ console.log(` \x1b[32m✓\x1b[0m Package manager: ${info.packageManager}`);
284
+ console.log();
285
+ console.log(" Checking external tools...");
286
+ const tools = checkExternalTools();
287
+ let hasWarning = false;
288
+ for (const tool of tools) if (tool.installed) console.log(` \x1b[32m✓\x1b[0m ${tool.name} — found`);
289
+ else {
290
+ hasWarning = true;
291
+ console.log(` \x1b[33m⚠\x1b[0m ${tool.name} — not found`);
292
+ console.log(` Install: ${tool.hint}`);
293
+ }
294
+ if (!hasWarning) console.log(" All tools ready!");
295
+ let client;
296
+ if (options.client) client = options.client;
297
+ else if (interactive) client = (await select("? Which MCP client do you use?", MCP_CLIENTS, (c) => c.label)).value;
298
+ else client = "cursor";
299
+ console.log();
300
+ console.log(" Applying changes...");
301
+ const babelResult = updateBabelConfig(info);
302
+ if (babelResult.success) console.log(` \x1b[32m✓\x1b[0m ${info.babelConfigPath ? path.basename(info.babelConfigPath) : "babel.config.js"} — ${babelResult.message}`);
303
+ else console.log(` \x1b[33m⚠\x1b[0m babel.config.js — ${babelResult.message}`);
304
+ const mcpResult = setupMcpConfig(client, cwd);
305
+ if (mcpResult.success) console.log(` \x1b[32m✓\x1b[0m MCP config — ${mcpResult.message}`);
306
+ else console.log(` \x1b[31m✗\x1b[0m MCP config — ${mcpResult.message}`);
307
+ updateGitignore(cwd);
308
+ console.log();
309
+ console.log("\x1B[32m Done!\x1B[0m Next steps:");
310
+ if (info.isExpo) console.log(" 1. Start your app: npx expo start");
311
+ else console.log(" 1. Start Metro: REACT_NATIVE_MCP_ENABLED=true npx react-native start");
312
+ const clientLabel = MCP_CLIENTS.find((c) => c.value === client)?.label ?? client;
313
+ console.log(` 2. Open ${clientLabel} — MCP tools are ready to use`);
314
+ console.log();
315
+ closeRL();
316
+ }
317
+ function updateGitignore(cwd) {
318
+ const gitignorePath = path.join(cwd, ".gitignore");
319
+ if (!fs.existsSync(gitignorePath)) return;
320
+ const content = fs.readFileSync(gitignorePath, "utf-8");
321
+ if (content.includes("/results/") || content.includes("results/")) {
322
+ console.log(" \x1B[32m✓\x1B[0m .gitignore — already has results/");
323
+ return;
324
+ }
325
+ fs.appendFileSync(gitignorePath, "\n# React Native MCP\n/results/\n", "utf-8");
326
+ console.log(" \x1B[32m✓\x1B[0m .gitignore — updated");
327
+ }
328
+
329
+ //#endregion
330
+ export { runInit };
@@ -0,0 +1,44 @@
1
+ //#region src/metro/transform-source.d.ts
2
+ /**
3
+ * Metro 커스텀 transformer용 소스 변환 (AST 기반)
4
+ *
5
+ * 앱 진입점에서 AppRegistry.registerComponent 호출을 찾아
6
+ * MCP 런타임 래퍼로 감싸서, CLI 테스트로 동작 여부를 검증할 수 있게 한다.
7
+ */
8
+ /**
9
+ * 소스 코드를 AST로 파싱한 뒤:
10
+ * 1) 진입점 상단에 MCP 런타임 require 주입
11
+ * 2) AppRegistry.registerComponent → __REACT_NATIVE_MCP__.registerComponent 치환
12
+ *
13
+ * @param src - 변환할 소스 문자열
14
+ * @param filename - 파일 경로 (소스맵용, 선택)
15
+ * @returns 변환된 code (그리고 추후 sourceMap)
16
+ */
17
+ declare function transformSource(src: string, filename?: string): Promise<{
18
+ code: string;
19
+ }>;
20
+ //#endregion
21
+ //#region src/babel/inject-testid.d.ts
22
+ /**
23
+ * Babel AST 변환: JSX 요소에 자동 testID 주입 + displayName 보존
24
+ *
25
+ * DESIGN.md Phase 3 계획에 따른 자동 testID 생성.
26
+ * 커스텀 컴포넌트 내부의 JSX 요소 중 testID가 없으면
27
+ * ComponentName-index-TagName 형식으로 주입한다.
28
+ *
29
+ * PascalCase 함수 컴포넌트에 displayName을 자동 주입해
30
+ * release 빌드에서도 Fiber 트리에서 컴포넌트 이름을 보존한다.
31
+ */
32
+ /**
33
+ * 소스 코드를 파싱한 뒤, 컴포넌트 함수 내부의 JSX 요소에
34
+ * testID가 없으면 자동으로 주입한다.
35
+ *
36
+ * @param src - 변환할 소스 문자열
37
+ * @param filename - 파일 경로 (선택)
38
+ * @returns 변환된 code
39
+ */
40
+ declare function injectTestIds(src: string, filename?: string): Promise<{
41
+ code: string;
42
+ }>;
43
+ //#endregion
44
+ export { injectTestIds, transformSource };