@ceraph/react-native-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ceraph/react-native-mcp — MCP server for React Native / Expo development workflow.
4
+ *
5
+ * Auto-detects Expo vs bare React Native projects and uses the appropriate
6
+ * commands. Provides tools for building, running, error capture, screen
7
+ * interaction, and prebuild detection.
8
+ */
9
+ import { join } from "node:path";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { z } from "zod";
13
+ import { RNManager } from "./expo-manager.js";
14
+ import { ScreenManager } from "./screen.js";
15
+ import { PrebuildDetector } from "./prebuild-detector.js";
16
+ // ---------------------------------------------------------------------------
17
+ // Resolve the project working directory.
18
+ // ---------------------------------------------------------------------------
19
+ // Cache lives next to the consumer's project (where the RN/Expo app actually
20
+ // lives) so the prebuild snapshot persists across MCP restarts and across
21
+ // different install paths (npx cache vs. local checkout).
22
+ const PROJECT_DIR = process.cwd();
23
+ const CACHE_DIR = join(PROJECT_DIR, ".rn-mcp-cache");
24
+ // ---------------------------------------------------------------------------
25
+ // Initialize managers
26
+ // ---------------------------------------------------------------------------
27
+ const rnManager = new RNManager(PROJECT_DIR);
28
+ const screenManager = new ScreenManager();
29
+ const prebuildDetector = new PrebuildDetector(PROJECT_DIR, CACHE_DIR);
30
+ // ---------------------------------------------------------------------------
31
+ // Create MCP server
32
+ // ---------------------------------------------------------------------------
33
+ const server = new McpServer({
34
+ name: "react-native-mcp",
35
+ version: "0.1.0",
36
+ }, {
37
+ capabilities: {
38
+ tools: {},
39
+ },
40
+ instructions: "React Native / Expo development workflow tools. Auto-detects project type. " +
41
+ "Use rn_build_ios to build, rn_start to launch Metro, rn_get_errors to " +
42
+ "check for problems, screen_tap and screen_find_and_tap for device " +
43
+ "interaction with automatic pixel ratio correction, and rn_check_prebuild " +
44
+ "to detect when a clean native rebuild is needed (Expo only).",
45
+ });
46
+ // ---------------------------------------------------------------------------
47
+ // Tool: rn_build_ios
48
+ // ---------------------------------------------------------------------------
49
+ server.tool("rn_build_ios", "Build and run the React Native app on an iOS device or simulator. " +
50
+ "Auto-detects Expo vs bare React Native and uses the correct command. " +
51
+ "Captures Xcode build output and returns structured error information. " +
52
+ "If clean is true, runs `npx expo prebuild --clean` first (Expo only).", {
53
+ clean: z
54
+ .boolean()
55
+ .optional()
56
+ .describe("Run prebuild --clean before building (Expo only)"),
57
+ device: z
58
+ .string()
59
+ .optional()
60
+ .describe("Device UDID to target (e.g., 00008140-001958943A78801C)"),
61
+ }, async ({ clean, device }) => {
62
+ try {
63
+ const result = await rnManager.runBuild({ clean, device });
64
+ // On success, save a snapshot for future prebuild checks
65
+ if (result.success) {
66
+ await prebuildDetector.saveSnapshot().catch(() => {
67
+ // Non-critical; don't fail the build result over this
68
+ });
69
+ }
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: JSON.stringify(result, null, 2),
75
+ },
76
+ ],
77
+ isError: !result.success,
78
+ };
79
+ }
80
+ catch (err) {
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `rn_build_ios failed: ${err instanceof Error ? err.message : String(err)}`,
86
+ },
87
+ ],
88
+ isError: true,
89
+ };
90
+ }
91
+ });
92
+ // ---------------------------------------------------------------------------
93
+ // Tool: rn_start
94
+ // ---------------------------------------------------------------------------
95
+ server.tool("rn_start", "Start the Metro dev server. Auto-detects Expo vs bare React Native. " +
96
+ "Monitors console output for runtime errors and warnings.", {
97
+ port: z
98
+ .number()
99
+ .optional()
100
+ .describe("Port for Metro bundler (default: 8081)"),
101
+ clear: z
102
+ .boolean()
103
+ .optional()
104
+ .describe("Clear Metro bundler cache on start"),
105
+ }, async ({ port, clear }) => {
106
+ try {
107
+ const result = await rnManager.startMetro({ port, clear });
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: JSON.stringify(result, null, 2),
113
+ },
114
+ ],
115
+ isError: !result.success,
116
+ };
117
+ }
118
+ catch (err) {
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text: `rn_start failed: ${err instanceof Error ? err.message : String(err)}`,
124
+ },
125
+ ],
126
+ isError: true,
127
+ };
128
+ }
129
+ });
130
+ // ---------------------------------------------------------------------------
131
+ // Tool: rn_get_errors
132
+ // ---------------------------------------------------------------------------
133
+ server.tool("rn_get_errors", "Return all captured errors from build and runtime. " +
134
+ "Includes structured build errors (file, line, column, message), " +
135
+ "runtime JS errors (message, stack trace), and warnings.", {}, async () => {
136
+ try {
137
+ const errors = rnManager.getErrors();
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: JSON.stringify(errors, null, 2),
143
+ },
144
+ ],
145
+ };
146
+ }
147
+ catch (err) {
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: `rn_get_errors failed: ${err instanceof Error ? err.message : String(err)}`,
153
+ },
154
+ ],
155
+ isError: true,
156
+ };
157
+ }
158
+ });
159
+ // ---------------------------------------------------------------------------
160
+ // Tool: rn_get_console
161
+ // ---------------------------------------------------------------------------
162
+ server.tool("rn_get_console", "Return recent console output from Metro dev server, " +
163
+ "optionally filtered by log level.", {
164
+ lines: z
165
+ .number()
166
+ .optional()
167
+ .describe("Number of recent lines to return (default: 50)"),
168
+ level: z
169
+ .enum(["all", "error", "warn", "log"])
170
+ .optional()
171
+ .describe("Filter by log level (default: all)"),
172
+ }, async ({ lines, level }) => {
173
+ try {
174
+ const output = rnManager.getConsole({ lines, level });
175
+ if (output.length === 0) {
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: "No console output captured. Is Metro running? Use rn_start to launch it.",
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: output.join("\n"),
190
+ },
191
+ ],
192
+ };
193
+ }
194
+ catch (err) {
195
+ return {
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: `rn_get_console failed: ${err instanceof Error ? err.message : String(err)}`,
200
+ },
201
+ ],
202
+ isError: true,
203
+ };
204
+ }
205
+ });
206
+ // ---------------------------------------------------------------------------
207
+ // Tool: rn_check_prebuild
208
+ // ---------------------------------------------------------------------------
209
+ server.tool("rn_check_prebuild", "Check if a clean Expo prebuild is needed (Expo projects only). " +
210
+ "Compares current package.json dependencies, app.json config, and " +
211
+ "ios/Podfile.lock against a cached snapshot from the last successful build.", {}, async () => {
212
+ try {
213
+ const isExpo = await rnManager.detectProjectType();
214
+ if (!isExpo) {
215
+ return {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: JSON.stringify({
220
+ needsClean: false,
221
+ reasons: ["Not an Expo project. Prebuild detection is Expo-only."],
222
+ }),
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ const result = await prebuildDetector.check();
228
+ return {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: JSON.stringify(result, null, 2),
233
+ },
234
+ ],
235
+ };
236
+ }
237
+ catch (err) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `rn_check_prebuild failed: ${err instanceof Error ? err.message : String(err)}`,
243
+ },
244
+ ],
245
+ isError: true,
246
+ };
247
+ }
248
+ });
249
+ // ---------------------------------------------------------------------------
250
+ // Tool: screen_tap
251
+ // ---------------------------------------------------------------------------
252
+ server.tool("screen_tap", "Tap at specific coordinates on the iOS device screen via WebDriverAgent. " +
253
+ "Automatically corrects for pixel ratio mismatch when coordinates come " +
254
+ "from a screenshot (divides by device pixel ratio). " +
255
+ "Requires WebDriverAgent running on localhost:8100.", {
256
+ x: z.number().describe("X coordinate to tap"),
257
+ y: z.number().describe("Y coordinate to tap"),
258
+ coordinateSource: z
259
+ .enum(["screenshot", "device"])
260
+ .optional()
261
+ .describe("Source of coordinates: 'screenshot' (default) auto-corrects " +
262
+ "for pixel ratio; 'device' uses coordinates as-is"),
263
+ }, async ({ x, y, coordinateSource }) => {
264
+ try {
265
+ const available = await screenManager.isAvailable();
266
+ if (!available) {
267
+ return {
268
+ content: [
269
+ {
270
+ type: "text",
271
+ text: JSON.stringify({
272
+ success: false,
273
+ error: "WebDriverAgent is not reachable at localhost:8100. " +
274
+ "Ensure it is running on the device.",
275
+ tappedAt: { x, y },
276
+ pixelRatio: 0,
277
+ correction: "N/A",
278
+ }),
279
+ },
280
+ ],
281
+ isError: true,
282
+ };
283
+ }
284
+ const fromScreenshot = (coordinateSource ?? "screenshot") === "screenshot";
285
+ const result = await screenManager.tap(x, y, fromScreenshot);
286
+ return {
287
+ content: [
288
+ {
289
+ type: "text",
290
+ text: JSON.stringify(result, null, 2),
291
+ },
292
+ ],
293
+ isError: !result.success,
294
+ };
295
+ }
296
+ catch (err) {
297
+ return {
298
+ content: [
299
+ {
300
+ type: "text",
301
+ text: `screen_tap failed: ${err instanceof Error ? err.message : String(err)}`,
302
+ },
303
+ ],
304
+ isError: true,
305
+ };
306
+ }
307
+ });
308
+ // ---------------------------------------------------------------------------
309
+ // Tool: screen_find_and_tap
310
+ // ---------------------------------------------------------------------------
311
+ server.tool("screen_find_and_tap", "Find a UI element on the iOS device screen by text, accessibility label, " +
312
+ "or element type, then tap its center. Uses the WebDriverAgent element tree. " +
313
+ "If no match is found, returns a summary of visible elements for debugging. " +
314
+ "Requires WebDriverAgent running on localhost:8100.", {
315
+ text: z
316
+ .string()
317
+ .optional()
318
+ .describe("Text content to search for (case-insensitive substring match)"),
319
+ accessibilityLabel: z
320
+ .string()
321
+ .optional()
322
+ .describe("Accessibility label to search for (case-insensitive substring match)"),
323
+ type: z
324
+ .string()
325
+ .optional()
326
+ .describe("Element type to filter by (e.g., XCUIElementTypeButton, XCUIElementTypeStaticText)"),
327
+ index: z
328
+ .number()
329
+ .optional()
330
+ .describe("If multiple matches, tap the nth match (0-indexed, default: 0)"),
331
+ }, async ({ text, accessibilityLabel, type, index }) => {
332
+ try {
333
+ const available = await screenManager.isAvailable();
334
+ if (!available) {
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: JSON.stringify({
340
+ success: false,
341
+ error: "WebDriverAgent is not reachable at localhost:8100. " +
342
+ "Ensure it is running on the device.",
343
+ }),
344
+ },
345
+ ],
346
+ isError: true,
347
+ };
348
+ }
349
+ if (!text && !accessibilityLabel && !type) {
350
+ return {
351
+ content: [
352
+ {
353
+ type: "text",
354
+ text: JSON.stringify({
355
+ success: false,
356
+ error: "At least one of text, accessibilityLabel, or type must be provided.",
357
+ }),
358
+ },
359
+ ],
360
+ isError: true,
361
+ };
362
+ }
363
+ const result = await screenManager.findAndTap({
364
+ text,
365
+ accessibilityLabel,
366
+ type,
367
+ index,
368
+ });
369
+ return {
370
+ content: [
371
+ {
372
+ type: "text",
373
+ text: JSON.stringify(result, null, 2),
374
+ },
375
+ ],
376
+ isError: !result.success,
377
+ };
378
+ }
379
+ catch (err) {
380
+ return {
381
+ content: [
382
+ {
383
+ type: "text",
384
+ text: `screen_find_and_tap failed: ${err instanceof Error ? err.message : String(err)}`,
385
+ },
386
+ ],
387
+ isError: true,
388
+ };
389
+ }
390
+ });
391
+ // ---------------------------------------------------------------------------
392
+ // Tool: rn_stop
393
+ // ---------------------------------------------------------------------------
394
+ server.tool("rn_stop", "Stop all managed React Native processes (Metro dev server and/or build process).", {}, async () => {
395
+ try {
396
+ const stopped = await rnManager.stopAll();
397
+ if (stopped.length === 0) {
398
+ return {
399
+ content: [
400
+ {
401
+ type: "text",
402
+ text: JSON.stringify({
403
+ stopped: [],
404
+ message: "No managed processes were running.",
405
+ }),
406
+ },
407
+ ],
408
+ };
409
+ }
410
+ return {
411
+ content: [
412
+ {
413
+ type: "text",
414
+ text: JSON.stringify({ stopped }, null, 2),
415
+ },
416
+ ],
417
+ };
418
+ }
419
+ catch (err) {
420
+ return {
421
+ content: [
422
+ {
423
+ type: "text",
424
+ text: `rn_stop failed: ${err instanceof Error ? err.message : String(err)}`,
425
+ },
426
+ ],
427
+ isError: true,
428
+ };
429
+ }
430
+ });
431
+ // ---------------------------------------------------------------------------
432
+ // Start the server
433
+ // ---------------------------------------------------------------------------
434
+ async function main() {
435
+ const transport = new StdioServerTransport();
436
+ await server.connect(transport);
437
+ console.error("[react-native-mcp] Server started on stdio transport");
438
+ }
439
+ main().catch((err) => {
440
+ console.error("[react-native-mcp] Fatal error:", err);
441
+ process.exit(1);
442
+ });
package/dist/init.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ceraph/react-native-mcp init
4
+ *
5
+ * Sets up MCP configuration and the runtime error hook for the current project.
6
+ * Detects which MCP clients are in use and writes config for each one.
7
+ */
8
+ export {};
package/dist/init.js ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ceraph/react-native-mcp init
4
+ *
5
+ * Sets up MCP configuration and the runtime error hook for the current project.
6
+ * Detects which MCP clients are in use and writes config for each one.
7
+ */
8
+ import { readFile, writeFile, mkdir, access, chmod } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ const PROJECT_DIR = process.cwd();
11
+ // ---------------------------------------------------------------------------
12
+ // Hook script content
13
+ // ---------------------------------------------------------------------------
14
+ const HOOK_SCRIPT = `#!/bin/bash
15
+ # rn-error-notify.sh — Injected by @ceraph/react-native-mcp init
16
+ # Reads .rn-errors.json and injects runtime errors into Claude's context.
17
+
18
+ ERROR_FILE="\$CLAUDE_PROJECT_DIR/mobile/.rn-errors.json"
19
+
20
+ # Also check project root if mobile/ doesn't exist
21
+ if [ ! -f "\$ERROR_FILE" ]; then
22
+ ERROR_FILE="\$CLAUDE_PROJECT_DIR/.rn-errors.json"
23
+ fi
24
+
25
+ if [ ! -f "\$ERROR_FILE" ]; then
26
+ exit 0
27
+ fi
28
+
29
+ ERROR_COUNT=\$(jq -r '.errors | length' "\$ERROR_FILE" 2>/dev/null)
30
+
31
+ if [ "\$ERROR_COUNT" = "0" ] || [ -z "\$ERROR_COUNT" ]; then
32
+ exit 0
33
+ fi
34
+
35
+ echo "REACT NATIVE RUNTIME ERROR DETECTED:"
36
+ echo ""
37
+ jq -r '.errors[] | "Error: \\(.message)\\nStack: \\(.stack)\\nTime: \\(.timestamp)\\n---"' "\$ERROR_FILE" 2>/dev/null
38
+ echo ""
39
+ echo "Use rn_get_errors for full details. Fix the error and rebuild."
40
+ `;
41
+ // ---------------------------------------------------------------------------
42
+ // MCP server entry (same for all JSON-based clients)
43
+ // ---------------------------------------------------------------------------
44
+ const MCP_ENTRY = {
45
+ "mobile-mcp": {
46
+ command: "npx",
47
+ args: ["-y", "@mobilenext/mobile-mcp@latest"],
48
+ },
49
+ "react-native-mcp": {
50
+ command: "npx",
51
+ args: ["-y", "@ceraph/react-native-mcp@latest"],
52
+ },
53
+ };
54
+ // ---------------------------------------------------------------------------
55
+ // Helpers
56
+ // ---------------------------------------------------------------------------
57
+ async function fileExists(path) {
58
+ try {
59
+ await access(path);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ async function readJson(path) {
67
+ try {
68
+ const content = await readFile(path, "utf-8");
69
+ return JSON.parse(content);
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ async function writeJson(path, data) {
76
+ await mkdir(join(path, ".."), { recursive: true });
77
+ await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
78
+ }
79
+ function log(msg) {
80
+ console.log(` ${msg}`);
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // MCP config writers
84
+ // ---------------------------------------------------------------------------
85
+ async function setupJsonMcp(configPath, label) {
86
+ const existing = (await readJson(configPath)) ?? {};
87
+ const servers = (existing.mcpServers ?? {});
88
+ if (servers["react-native-mcp"]) {
89
+ log(`${label}: already configured`);
90
+ return false;
91
+ }
92
+ servers["mobile-mcp"] = MCP_ENTRY["mobile-mcp"];
93
+ servers["react-native-mcp"] = MCP_ENTRY["react-native-mcp"];
94
+ existing.mcpServers = servers;
95
+ await writeJson(configPath, existing);
96
+ log(`${label}: configured ✓`);
97
+ return true;
98
+ }
99
+ async function setupCodexToml(configPath) {
100
+ let content = "";
101
+ try {
102
+ content = await readFile(configPath, "utf-8");
103
+ }
104
+ catch {
105
+ // File doesn't exist, we'll create it
106
+ }
107
+ if (content.includes("react-native-mcp")) {
108
+ log("Codex: already configured");
109
+ return false;
110
+ }
111
+ const tomlBlock = `
112
+ [mcp_servers.mobile-mcp]
113
+ command = "npx"
114
+ args = ["-y", "@mobilenext/mobile-mcp@latest"]
115
+
116
+ [mcp_servers.react-native-mcp]
117
+ command = "npx"
118
+ args = ["-y", "@ceraph/react-native-mcp@latest"]
119
+ `;
120
+ await mkdir(join(configPath, ".."), { recursive: true });
121
+ await writeFile(configPath, content + tomlBlock, "utf-8");
122
+ log("Codex: configured ✓");
123
+ return true;
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Hook setup
127
+ // ---------------------------------------------------------------------------
128
+ async function setupClaudeHook() {
129
+ // Write the hook script
130
+ const hooksDir = join(PROJECT_DIR, ".claude", "hooks");
131
+ const hookPath = join(hooksDir, "rn-error-notify.sh");
132
+ await mkdir(hooksDir, { recursive: true });
133
+ await writeFile(hookPath, HOOK_SCRIPT, "utf-8");
134
+ await chmod(hookPath, 0o755);
135
+ log("Hook script: .claude/hooks/rn-error-notify.sh ✓");
136
+ // Add FileChanged hook to settings.
137
+ // Check both settings.json and settings.local.json — if either already has
138
+ // the hook, we treat it as configured. This prevents duplicate hooks (which
139
+ // would fire twice per .rn-errors.json write) for projects where the hook
140
+ // was originally written to settings.local.json.
141
+ const sharedSettingsPath = join(PROJECT_DIR, ".claude", "settings.json");
142
+ const localSettingsPath = join(PROJECT_DIR, ".claude", "settings.local.json");
143
+ const isMatcher = (h) => h.matcher === ".rn-errors.json";
144
+ const fileChangedFrom = (s) => {
145
+ const hooks = (s?.hooks ?? {});
146
+ return (hooks.FileChanged ?? []);
147
+ };
148
+ const sharedSettings = await readJson(sharedSettingsPath);
149
+ const localSettings = await readJson(localSettingsPath);
150
+ if (fileChangedFrom(sharedSettings).some(isMatcher) ||
151
+ fileChangedFrom(localSettings).some(isMatcher)) {
152
+ log("Claude Code hook: already configured");
153
+ return;
154
+ }
155
+ // Write to settings.json (the shared default). settings.local.json is
156
+ // reserved for per-machine overrides; we don't write there.
157
+ const settings = sharedSettings ?? {};
158
+ const hooks = (settings.hooks ?? {});
159
+ const fileChangedHooks = (hooks.FileChanged ?? []);
160
+ fileChangedHooks.push({
161
+ matcher: ".rn-errors.json",
162
+ hooks: [
163
+ {
164
+ type: "command",
165
+ command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/rn-error-notify.sh',
166
+ },
167
+ ],
168
+ });
169
+ hooks.FileChanged = fileChangedHooks;
170
+ settings.hooks = hooks;
171
+ await writeJson(sharedSettingsPath, settings);
172
+ log("Claude Code hook: FileChanged → .rn-errors.json ✓");
173
+ }
174
+ // ---------------------------------------------------------------------------
175
+ // Gitignore
176
+ // ---------------------------------------------------------------------------
177
+ async function setupGitignore() {
178
+ const gitignorePath = join(PROJECT_DIR, ".gitignore");
179
+ let content = "";
180
+ try {
181
+ content = await readFile(gitignorePath, "utf-8");
182
+ }
183
+ catch {
184
+ // No .gitignore yet
185
+ }
186
+ const entries = [".rn-errors.json", ".rn-mcp-cache/"];
187
+ const missing = entries.filter((e) => !content.includes(e));
188
+ if (missing.length === 0) {
189
+ return;
190
+ }
191
+ const prefix = content === "" || content.endsWith("\n") ? "" : "\n";
192
+ const addition = prefix + missing.join("\n") + "\n";
193
+ await writeFile(gitignorePath, content + addition, "utf-8");
194
+ log(`.gitignore: added ${missing.join(", ")} ✓`);
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Main
198
+ // ---------------------------------------------------------------------------
199
+ async function main() {
200
+ console.log("\n@ceraph/react-native-mcp init\n");
201
+ // Detect and configure MCP clients
202
+ console.log("MCP configuration:");
203
+ // Claude Code
204
+ await setupJsonMcp(join(PROJECT_DIR, ".mcp.json"), "Claude Code");
205
+ // Cursor
206
+ await setupJsonMcp(join(PROJECT_DIR, ".cursor", "mcp.json"), "Cursor");
207
+ // Codex
208
+ await setupCodexToml(join(PROJECT_DIR, ".codex", "config.toml"));
209
+ // VS Code / Copilot
210
+ await setupJsonMcp(join(PROJECT_DIR, ".vscode", "mcp.json"), "VS Code");
211
+ // Windsurf (user-level config)
212
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
213
+ if (home) {
214
+ const windsurfPath = join(home, ".codeium", "windsurf", "mcp_config.json");
215
+ if (await fileExists(join(home, ".codeium", "windsurf"))) {
216
+ await setupJsonMcp(windsurfPath, "Windsurf");
217
+ }
218
+ // Antigravity (user-level config)
219
+ const antigravityPath = join(home, ".gemini", "antigravity", "mcp_config.json");
220
+ if (await fileExists(join(home, ".gemini", "antigravity"))) {
221
+ await setupJsonMcp(antigravityPath, "Antigravity");
222
+ }
223
+ }
224
+ // Claude Code error hook
225
+ console.log("\nRuntime error hook:");
226
+ await setupClaudeHook();
227
+ // Gitignore
228
+ console.log("\nGitignore:");
229
+ await setupGitignore();
230
+ console.log("\nDone. Runtime errors will be automatically injected into Claude's context.\n");
231
+ }
232
+ main().catch((err) => {
233
+ console.error("Init failed:", err);
234
+ process.exit(1);
235
+ });