nerve-mcp 0.1.1 → 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 CHANGED
@@ -43,6 +43,7 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
43
43
  const ws_1 = __importDefault(require("ws"));
44
44
  const child_process_1 = require("child_process");
45
45
  const path = __importStar(require("path"));
46
+ const fs = __importStar(require("fs"));
46
47
  const net = __importStar(require("net"));
47
48
  // --- Port Resolution ---
48
49
  // djb2 hash — must match the Swift side exactly
@@ -177,13 +178,13 @@ The tap= coordinate is the center point where the element is reliably hittable.
177
178
  ### Verify
178
179
  - nerve_view to see updated screen state — this is your PRIMARY inspection tool. It returns structured element data with refs, identifiers, and tap coordinates you can act on directly.
179
180
  - nerve_console with filter="[nerve]" and since="last_action" for your trace logs.
180
- - nerve_screenshot ONLY when you need to verify visual layout, colors, or spatial relationships that text can't convey. Do NOT use screenshot as a substitute for nerve_view.
181
+ - Do NOT use nerve_screenshot unless nerve_view is insufficient (e.g., verifying colors, gradients, or visual layout). When you must screenshot, ALWAYS crop to the relevant element: nerve_screenshot with element="#my-element" instead of capturing the full screen.
181
182
  - nerve_heap to inspect live objects (e.g., check ViewModel state).
182
183
 
183
184
  ### Tips
184
185
  - Always call nerve_view before interacting — don't guess element identifiers.
185
186
  - Use @eN refs from nerve_view output to tap elements without identifiers.
186
- - nerve_view is lightweight (~1 line per element) and gives you everything needed to interact. Prefer it over nerve_screenshot for all inspection tasks.
187
+ - nerve_view is lightweight (~1 line per element) and gives you everything needed to interact. NEVER use nerve_screenshot when nerve_view can answer the question.
187
188
  - If an element isn't visible, try nerve_scroll_to_find before giving up.
188
189
  - The navigation map builds automatically and persists across sessions.
189
190
  - Do NOT add sleep/delay between commands — Nerve handles waiting automatically.
@@ -371,13 +372,16 @@ const TOOLS = [
371
372
  },
372
373
  {
373
374
  name: "nerve_screenshot",
374
- description: "Capture a screenshot of the current screen. Returns base64-encoded PNG. Prefer nerve_view for understanding screen state and finding elements it returns structured data with element refs and tap coordinates. Only use screenshot for visual layout verification when text output isn't enough.",
375
+ description: "Capture a screenshot. WARNING: Screenshots consume significant tokens use nerve_view for all normal inspection. Only use for visual checks (colors, gradients, images, layout). Use 'element' to crop to a specific element (cheapest), or 'region' to crop to a normalized area. Avoid full-screen screenshots when possible.",
375
376
  inputSchema: {
376
377
  type: "object",
377
378
  properties: {
378
379
  target: { type: "string" },
380
+ element: { type: "string", description: "Crop to a specific element. Use @eN ref, #identifier, or @label from nerve_view. This is the most token-efficient way to verify visual appearance." },
381
+ region: { type: "string", description: "Crop to a normalized region: \"x1,y1,x2,y2\" where values are 0-1. Example: \"0,0,0.5,0.5\" for top-left quarter." },
382
+ padding: { type: "number", description: "Padding in points around the element crop. Default: 20." },
379
383
  scale: { type: "number", description: "Image scale. Default: 1.0." },
380
- maxDimension: { type: "number", description: "Resize so longest side fits within this value (in points). Normalizes across device sizes. Example: 800. Overrides scale when set." },
384
+ maxDimension: { type: "number", description: "Resize so longest side fits within this value (in points). Example: 800. Overrides scale when set." },
381
385
  },
382
386
  },
383
387
  },
@@ -530,7 +534,7 @@ const TOOLS = [
530
534
  },
531
535
  {
532
536
  name: "nerve_run",
533
- description: "Build, install, and launch an iOS app on the simulator. The app must include the Nerve SPM package. After launching, call nerve_view to see the initial screen, then navigate and interact as needed.",
537
+ description: "Build, install, and launch an iOS app on the simulator. Nerve is auto-injected if the app doesn't include it via SPM — no code changes needed. After launching, call nerve_view to see the initial screen.",
534
538
  inputSchema: {
535
539
  type: "object",
536
540
  properties: {
@@ -1087,6 +1091,60 @@ function runShell(cmd, timeoutMs = 120000) {
1087
1091
  proc.on("error", reject);
1088
1092
  });
1089
1093
  }
1094
+ // --- Nerve Framework Injection ---
1095
+ // Resolve paths for finding the Nerve framework
1096
+ // mcp-server/dist/index.js -> mcp-server/ (npm package root)
1097
+ // mcp-server/src/index.ts -> mcp-server/ (development)
1098
+ const mpcPackageRoot = path.resolve(__dirname, "..");
1099
+ const nerveRepoRoot = path.resolve(mpcPackageRoot, "..");
1100
+ function findNerveFramework() {
1101
+ const candidates = [
1102
+ // 1. Bundled with npm package (npm install nerve-mcp)
1103
+ path.join(mpcPackageRoot, "framework", "Nerve.framework", "Nerve"),
1104
+ // 2. Homebrew installation
1105
+ "/opt/homebrew/lib/nerve/Nerve.framework/Nerve",
1106
+ "/usr/local/lib/nerve/Nerve.framework/Nerve",
1107
+ // 3. Repo .build/inject/ (development from source)
1108
+ path.join(nerveRepoRoot, ".build", "inject", "Nerve.framework", "Nerve"),
1109
+ ];
1110
+ return candidates.find(p => fs.existsSync(p)) ?? null;
1111
+ }
1112
+ async function ensureNerveFramework() {
1113
+ const existing = findNerveFramework();
1114
+ if (existing)
1115
+ return existing;
1116
+ // Auto-build from source (development mode)
1117
+ const buildScript = path.join(nerveRepoRoot, "scripts", "build-framework.sh");
1118
+ if (fs.existsSync(buildScript)) {
1119
+ await runShell(`bash "${buildScript}"`, 180000);
1120
+ const built = findNerveFramework();
1121
+ if (built)
1122
+ return built;
1123
+ }
1124
+ throw new Error("Nerve.framework not found. If installed via npm, reinstall nerve-mcp. " +
1125
+ "If developing from source, run: scripts/build-framework.sh");
1126
+ }
1127
+ async function appContainsNerve(appPath) {
1128
+ // Check if the app binary contains Nerve symbols (works for both static and dynamic SPM linking)
1129
+ try {
1130
+ const appName = path.basename(appPath, ".app");
1131
+ const binary = path.join(appPath, appName);
1132
+ const symbols = await runShell(`nm -gU "${binary}" 2>/dev/null | grep nerve_auto_start || true`);
1133
+ if (symbols.trim())
1134
+ return true;
1135
+ // Also check Frameworks/ for dynamic linking case
1136
+ const frameworks = path.join(appPath, "Frameworks");
1137
+ if (fs.existsSync(frameworks)) {
1138
+ const entries = fs.readdirSync(frameworks);
1139
+ if (entries.some(e => e.startsWith("Nerve")))
1140
+ return true;
1141
+ }
1142
+ return false;
1143
+ }
1144
+ catch {
1145
+ return false;
1146
+ }
1147
+ }
1090
1148
  async function findSimulatorUDID(name) {
1091
1149
  const json = await runShell("xcrun simctl list devices available -j");
1092
1150
  const data = JSON.parse(json);
@@ -1177,9 +1235,19 @@ async function handleBuildRun(command, params) {
1177
1235
  catch {
1178
1236
  // Not running
1179
1237
  }
1180
- // Launch
1181
- await runShell(`xcrun simctl launch "${udid}" "${bundleId}"`);
1182
- log.push("Launched.");
1238
+ // Launch — detect SPM vs inject mode
1239
+ const hasNerve = await appContainsNerve(appPath);
1240
+ let injected = false;
1241
+ if (hasNerve) {
1242
+ await runShell(`xcrun simctl launch "${udid}" "${bundleId}"`);
1243
+ log.push("Launched (SPM mode).");
1244
+ }
1245
+ else {
1246
+ const frameworkBinary = await ensureNerveFramework();
1247
+ await runShell(`SIMCTL_CHILD_DYLD_INSERT_LIBRARIES="${frameworkBinary}" xcrun simctl launch "${udid}" "${bundleId}"`);
1248
+ log.push("Launched (inject mode).");
1249
+ injected = true;
1250
+ }
1183
1251
  // Set active target
1184
1252
  activeTarget = { udid, bundleId };
1185
1253
  const port = nervePort(udid, bundleId);
@@ -1194,7 +1262,12 @@ async function handleBuildRun(command, params) {
1194
1262
  await new Promise(r => setTimeout(r, 250));
1195
1263
  }
1196
1264
  if (!nerveReady) {
1197
- log.push("Nerve did not start. Ensure your app includes the Nerve SPM package with Nerve.start() in #if DEBUG.");
1265
+ if (injected) {
1266
+ log.push("Nerve did not start after injection. The framework may be incompatible — try rebuilding: delete .build/inject/ and re-run.");
1267
+ }
1268
+ else {
1269
+ log.push("Nerve did not start. Ensure your app calls Nerve.start() in #if DEBUG.");
1270
+ }
1198
1271
  }
1199
1272
  return { content: [{ type: "text", text: log.join("\n") }] };
1200
1273
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerve-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Nerve — gives AI agents runtime access to iOS apps",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -10,14 +10,15 @@
10
10
  "build": "tsc",
11
11
  "start": "node dist/index.js",
12
12
  "dev": "tsx src/index.ts",
13
- "prepublishOnly": "npm run build"
13
+ "prepublishOnly": "npm run build && npm run bundle-framework",
14
+ "bundle-framework": "bash ../scripts/build-framework.sh && rm -rf framework && mkdir -p framework/Nerve.framework && cp ../.build/inject/Nerve.framework/Nerve framework/Nerve.framework/ && cp ../.build/inject/Nerve.framework/Info.plist framework/Nerve.framework/"
14
15
  },
15
16
  "repository": {
16
17
  "type": "git",
17
18
  "url": "git+https://github.com/luchi0208/nerve-ios.git",
18
19
  "directory": "mcp-server"
19
20
  },
20
- "files": ["dist", "README.md"],
21
+ "files": ["dist", "framework", "README.md"],
21
22
  "keywords": ["mcp", "ios", "automation", "simulator", "nerve"],
22
23
  "dependencies": {
23
24
  "@modelcontextprotocol/sdk": "^1.0.0",