nerve-mcp 0.1.1 → 0.2.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 +208 -0
- package/dist/index.js +82 -9
- package/framework/Nerve.framework/Info.plist +0 -0
- package/framework/Nerve.framework/Nerve +0 -0
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Nerve
|
|
2
|
+
|
|
3
|
+
Nerve gives AI agents eyes and hands inside iOS apps.
|
|
4
|
+
|
|
5
|
+
Add the MCP server to your AI agent, and it can see every element on screen, tap buttons, fill forms, scroll, inspect state, intercept network calls, and debug your iOS app — all through natural language. No code changes needed.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### 1. Install the MCP Server
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx nerve-mcp@latest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install globally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g nerve-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or clone and build from source:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/luchi0208/nerve-ios.git
|
|
25
|
+
cd nerve/mcp-server && npm install && npm run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Configure Your AI Agent
|
|
29
|
+
|
|
30
|
+
**Claude Code** — add to your project's `.mcp.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"nerve": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["nerve-mcp@latest"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Claude Desktop / Cursor / Other MCP clients:**
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"nerve": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["nerve-mcp@latest"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If installed from source:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"nerve": {
|
|
62
|
+
"command": "node",
|
|
63
|
+
"args": ["/path/to/nerve/mcp-server/dist/index.js"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That's it. Tell your AI agent to build and run your app — Nerve auto-injects on the Simulator with no code changes needed.
|
|
70
|
+
|
|
71
|
+
## Example
|
|
72
|
+
|
|
73
|
+
Things you can ask your AI agent with Nerve:
|
|
74
|
+
|
|
75
|
+
> "Run my app on the simulator. Go to the checkout screen and try submitting an empty form — what validation errors show up?"
|
|
76
|
+
|
|
77
|
+
> "There's a bug where the cart badge doesn't update after removing an item. Can you reproduce it and check the console logs?"
|
|
78
|
+
|
|
79
|
+
> "Navigate through every screen in the app and find any buttons that don't respond to taps."
|
|
80
|
+
|
|
81
|
+
> "The login screen looks broken on iPhone SE. Run it on that simulator and screenshot just the login form so I can see what's wrong."
|
|
82
|
+
|
|
83
|
+
> "Trace all calls to `CartManager.addItem` and then add three items to the cart. Show me what arguments are being passed."
|
|
84
|
+
|
|
85
|
+
> "Check what's stored in UserDefaults after onboarding completes. I think we're saving the auth token in the wrong key."
|
|
86
|
+
|
|
87
|
+
> "Intercept the network requests when I pull to refresh on the orders screen. Show me the response bodies — I think the API is returning stale data."
|
|
88
|
+
|
|
89
|
+
These are just starting points. The agent combines Nerve's tools on its own — you describe what you want in plain English, and it figures out the sequence of taps, inspections, and checks to get there.
|
|
90
|
+
|
|
91
|
+
## How It Works
|
|
92
|
+
|
|
93
|
+
Nerve auto-injects into the app at launch on the Simulator — no code changes needed. It runs inside the app process, starts a WebSocket server, and the MCP server on the Mac connects to it. AI agent tool calls are translated into commands executed inside the app.
|
|
94
|
+
|
|
95
|
+
Because it runs in-process, Nerve has access to the full view hierarchy, the Objective-C runtime, live objects, network delegates, and the HID event system.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
AI Agent → MCP Server (Mac) → WebSocket → Nerve (in-app) → UIKit/SwiftUI
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Tools
|
|
102
|
+
|
|
103
|
+
### See the Screen
|
|
104
|
+
|
|
105
|
+
| Tool | Description |
|
|
106
|
+
|------|-------------|
|
|
107
|
+
| `nerve_view` | See all visible elements with type, label, ID, tap point, and position |
|
|
108
|
+
| `nerve_tree` | Full view hierarchy (UIKit + SwiftUI) |
|
|
109
|
+
| `nerve_inspect` | Detailed properties of a specific element |
|
|
110
|
+
| `nerve_screenshot` | Capture the screen as an image |
|
|
111
|
+
|
|
112
|
+
### Interact
|
|
113
|
+
|
|
114
|
+
| Tool | Description |
|
|
115
|
+
|------|-------------|
|
|
116
|
+
| `nerve_tap` | Tap an element by `@eN` ref, `#identifier`, `@label`, or coordinates |
|
|
117
|
+
| `nerve_type` | Type text into the focused field |
|
|
118
|
+
| `nerve_scroll` | Scroll in any direction |
|
|
119
|
+
| `nerve_swipe` | Swipe gesture |
|
|
120
|
+
| `nerve_long_press` | Long press |
|
|
121
|
+
| `nerve_double_tap` | Double tap |
|
|
122
|
+
| `nerve_drag_drop` | Drag from one element to another |
|
|
123
|
+
| `nerve_pull_to_refresh` | Pull to refresh |
|
|
124
|
+
| `nerve_pinch` | Pinch/zoom |
|
|
125
|
+
| `nerve_context_menu` | Open context menu |
|
|
126
|
+
| `nerve_back` | Navigate back |
|
|
127
|
+
| `nerve_dismiss` | Dismiss keyboard or modal |
|
|
128
|
+
|
|
129
|
+
### Navigate
|
|
130
|
+
|
|
131
|
+
| Tool | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `nerve_map` | See all discovered screens and transitions |
|
|
134
|
+
| `nerve_navigate` | Auto-navigate to a known screen |
|
|
135
|
+
| `nerve_scroll_to_find` | Scroll until an element appears |
|
|
136
|
+
| `nerve_deeplink` | Open a URL scheme |
|
|
137
|
+
|
|
138
|
+
### Inspect & Debug
|
|
139
|
+
|
|
140
|
+
| Tool | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `nerve_console` | App logs (stdout/stderr) |
|
|
143
|
+
| `nerve_network` | Intercepted HTTP traffic with response bodies |
|
|
144
|
+
| `nerve_heap` | Find live object instances by class name |
|
|
145
|
+
| `nerve_storage` | Read UserDefaults, Keychain, cookies, files |
|
|
146
|
+
| `nerve_trace` | Swizzle any method to log calls |
|
|
147
|
+
| `nerve_highlight` | Draw colored borders on elements for visual debugging |
|
|
148
|
+
| `nerve_modify` | Change view properties at runtime |
|
|
149
|
+
| `nerve_lldb` | Full LLDB debugger access |
|
|
150
|
+
|
|
151
|
+
### Build & Launch
|
|
152
|
+
|
|
153
|
+
| Tool | Description |
|
|
154
|
+
|------|-------------|
|
|
155
|
+
| `nerve_run` | Build, install, and launch on the simulator (auto-injects Nerve) |
|
|
156
|
+
| `nerve_build` | Build only |
|
|
157
|
+
| `nerve_status` | Show connected targets |
|
|
158
|
+
| `nerve_list_simulators` | List available simulators |
|
|
159
|
+
| `nerve_boot_simulator` | Boot a simulator by name or UDID |
|
|
160
|
+
| `nerve_appearance` | Switch between light and dark mode |
|
|
161
|
+
| `nerve_grant_permissions` | Pre-grant iOS permissions |
|
|
162
|
+
|
|
163
|
+
## Element Queries
|
|
164
|
+
|
|
165
|
+
Nerve supports several query formats for targeting elements:
|
|
166
|
+
|
|
167
|
+
| Format | Example | Description |
|
|
168
|
+
|--------|---------|-------------|
|
|
169
|
+
| `@eN` | `@e2` | Element ref from `nerve_view` output |
|
|
170
|
+
| `#id` | `#login-btn` | Accessibility identifier |
|
|
171
|
+
| `@label` | `@Settings` | Accessibility label |
|
|
172
|
+
| `.type:index` | `.field:0` | Element type with index |
|
|
173
|
+
| `x,y` | `195,160` | Screen coordinates |
|
|
174
|
+
|
|
175
|
+
The `nerve_view` output shows each element with its ref and tap point:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
@e1 btn "Product A" #product-a tap=195,222 x=16 y=195 w=358 h=54
|
|
179
|
+
@e2 field val=Email tap=195,160 x=32 y=149 w=326 h=22
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Use `@e2` to tap that field — Nerve uses the element's activation point (center), which is always the correct hittable position.
|
|
183
|
+
|
|
184
|
+
## Architecture
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Nerve/
|
|
188
|
+
Sources/
|
|
189
|
+
Nerve/ Swift framework — commands, element resolution, inspection
|
|
190
|
+
NerveObjC/ ObjC/C bridge — touch synthesis, heap walking, swizzling
|
|
191
|
+
Example/ Example app with test views
|
|
192
|
+
Tests/
|
|
193
|
+
E2E/ End-to-end tests (83 tests)
|
|
194
|
+
NerveTests/ Unit tests
|
|
195
|
+
mcp-server/ MCP server (TypeScript)
|
|
196
|
+
cli/ CLI tool
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Requirements
|
|
200
|
+
|
|
201
|
+
- macOS 14+
|
|
202
|
+
- Xcode 16+
|
|
203
|
+
- iOS Simulator (iOS 16+)
|
|
204
|
+
- Node.js 18+
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
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
|
|
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.
|
|
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
|
|
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).
|
|
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.
|
|
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
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nerve-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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 && cp ../README.md README.md",
|
|
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",
|