@synnode/expo-metro-mcp 1.0.3 → 1.0.7

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.
Files changed (3) hide show
  1. package/README.md +99 -6
  2. package/dist/index.js +40 -16
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -32,6 +32,22 @@ Restart Claude Code after adding the server.
32
32
  METRO_PORT=8081
33
33
  METRO_HOST=localhost
34
34
  LOG_BUFFER_SIZE=1000
35
+ EXPO_METRO_MCP_ENABLE_EVAL=0
36
+ # optional, comma-separated key prefixes
37
+ EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST=
38
+ ```
39
+
40
+ Additional safety toggles:
41
+
42
+ ```bash
43
+ # do not register the raw Runtime.evaluate tool unless you opt in
44
+ EXPO_METRO_MCP_ENABLE_EVAL=1
45
+
46
+ # disable MMKV / Zustand write helpers
47
+ EXPO_METRO_MCP_READ_ONLY=1
48
+
49
+ # only allow specific MMKV key prefixes (applies to reads and writes)
50
+ EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST=debug.,persist:dev:
35
51
  ```
36
52
 
37
53
  If Metro runs on a different port:
@@ -54,34 +70,111 @@ claude mcp add expo-metro --env METRO_PORT=8082 npx @synnode/expo-metro-mcp
54
70
  | `reload` | Reload the React Native app via Metro |
55
71
  | `resolve_stack` | Resolve a stack trace against the Metro source map, showing original file/line instead of bundle offsets |
56
72
  | `list_devices` | List active iOS simulators and Android emulators |
57
- | `screenshot` | Take a screenshot of the active simulator/emulator. Returns the image directly. Optional: `platform`, `device_id` |
73
+ | `screenshot` | Take a screenshot of the active simulator/emulator. Returns the image + pixel dimensions. Optional: `platform`, `device_id` |
58
74
  | `tap` | Tap at x,y coordinates on the active simulator/emulator. Optional: `platform`, `device_id` |
59
75
  | `swipe` | Swipe from one coordinate to another. Optional: `duration_ms`, `platform`, `device_id` |
76
+ | `input_text` | Type text into the focused input field — works without the on-screen keyboard. Optional: `platform`, `device_id` |
77
+ | `input_key` | Send a special key press: `enter`, `backspace`, `delete`, `tab`, `escape`, `back`, `space`, arrow keys. Optional: `platform`, `device_id` |
78
+ | `evaluate` | Run JavaScript inside the connected app runtime via Metro CDP. **Only registered when `EXPO_METRO_MCP_ENABLE_EVAL=1`.** Supports async expressions. Useful for reading state, calling app helpers, poking navigation, or mutating debug state. Optional: `timeout_ms` |
79
+ | `mmkv_get` | Read a raw string value from a dev-only MMKV debug hook exposed at `globalThis.__EXPO_METRO_MCP__.mmkv` |
80
+ | `mmkv_set` | Write a raw string value through that MMKV debug hook |
81
+ | `mmkv_remove` | Remove a key through that MMKV debug hook |
82
+ | `mmkv_keys` | List all keys available through that MMKV debug hook |
83
+ | `mmkv_get_json` | Read and parse a JSON-valued MMKV entry |
84
+ | `mmkv_set_json` | Store any JSON-serializable value in MMKV without manual stringifying |
85
+ | `mmkv_merge_json` | Shallow-merge an object into an existing JSON-valued MMKV entry |
86
+ | `zustand_persist_get` | Read a persisted Zustand MMKV payload and split out `state` and `version` |
87
+ | `zustand_persist_set` | Write a persisted Zustand payload in `{ state, version? }` shape |
88
+ | `zustand_persist_merge` | Merge fields into an existing persisted Zustand `state` object |
89
+
90
+ ## Runtime evaluation
91
+
92
+ `evaluate` runs JavaScript directly inside the connected React Native app runtime through CDP `Runtime.evaluate`.
93
+
94
+ **What it's good for:**
95
+ - inspect globals, stores, and navigation state
96
+ - call debug helpers or exported functions
97
+ - read or write app persistence through your app's JS runtime
98
+ - toggle feature flags or temporary state during debugging
99
+ - verify assumptions without rebuilding UI automation flows
100
+
101
+ **Notes:**
102
+ - Expressions are awaited automatically, so `Promise` results work out of the box
103
+ - Returned values are serialized when possible; non-serializable objects fall back to their runtime description
104
+ - This is a sharp tool. Great for development, mildly cursed in the wrong hands
105
+ - The MCP does **not** sandbox eval internally; treat this as arbitrary code execution in the app runtime
106
+ - To hide the tool entirely unless you explicitly want it, leave `EXPO_METRO_MCP_ENABLE_EVAL` unset
107
+
108
+ ## MMKV debug hook
109
+
110
+ If your app uses `react-native-mmkv`, you can expose a tiny dev-only hook and let the MCP seed or inspect persisted state without hand-written eval snippets.
111
+
112
+ Example app-side hook:
113
+
114
+ ```ts
115
+ import {createMMKV} from "react-native-mmkv";
116
+
117
+ export const mmkv = createMMKV({
118
+ id: "demo-app",
119
+ });
120
+
121
+ if (__DEV__) {
122
+ globalThis.__EXPO_METRO_MCP__ = {
123
+ ...globalThis.__EXPO_METRO_MCP__,
124
+ mmkv: {
125
+ id: "demo-app",
126
+ getItem: (key: string) => mmkv.getString(key) ?? null,
127
+ setItem: (key: string, value: string) => mmkv.set(key, value),
128
+ removeItem: (key: string) => mmkv.remove(key),
129
+ getAllKeys: () => mmkv.getAllKeys(),
130
+ },
131
+ };
132
+ }
133
+ ```
134
+
135
+ Once exposed, the MCP can use:
136
+ - low-level MMKV tools: `mmkv_get`, `mmkv_set`, `mmkv_remove`, `mmkv_keys`
137
+ - JSON helpers: `mmkv_get_json`, `mmkv_set_json`, `mmkv_merge_json`
138
+ - Zustand helpers: `zustand_persist_get`, `zustand_persist_set`, `zustand_persist_merge`
139
+
140
+ Safety notes:
141
+ - `EXPO_METRO_MCP_READ_ONLY=1` disables all MMKV/Zustand write helpers
142
+ - `EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST` limits MMKV/Zustand access to specific key prefixes
143
+ - `mmkv_keys` returns only allowed keys when a prefix allowlist is configured
144
+
145
+ This stays intentionally generic, so it works for persisted Zustand state and plain MMKV usage without coupling the MCP to your store internals.
146
+
147
+ For most AI-driven state seeding, the Zustand helpers are the sweet spot. They avoid hand-building the persisted wrapper shape every time.
60
148
 
61
149
  ## Screenshot & UI automation
62
150
 
63
- `screenshot`, `tap`, and `swipe` interact directly with your running simulator or emulator — no extra packages or paid plans needed.
151
+ `screenshot`, `tap`, `swipe`, `input_text`, and `input_key` interact directly with your running simulator or emulator — no extra packages or paid plans needed.
64
152
 
65
153
  **Requirements:**
66
154
  - **iOS screenshots**: macOS with Xcode installed (`xcrun simctl` must be available)
67
- - **iOS tap/swipe**: `idb` — Facebook's iOS Development Bridge
155
+ - **iOS tap/swipe/input**: `idb` — Facebook's iOS Development Bridge
68
156
  ```bash
69
157
  brew tap facebook/fb && brew install idb-companion
70
158
  pip3 install fb-idb
71
159
  ```
72
- - **Android**: `adb` in your PATH (part of Android SDK platform-tools) — tap, swipe and screenshot all work out of the box
160
+ - **Android**: `adb` in your PATH (part of Android SDK platform-tools) — all tools work out of the box
73
161
 
74
162
  **Notes:**
163
+ - `screenshot` returns the image alongside its pixel dimensions — use those coordinates directly for `tap`/`swipe`, no manual scaling needed
164
+ - `input_text` types into the focused field without requiring the on-screen keyboard to appear
165
+ - To fill a form: `tap` the field → `input_text` the value → `input_key "enter"` to submit
75
166
  - If multiple devices are running, use `list_devices` to find the ID and pass it via `device_id`
76
- - Coordinates are in points (iOS logical pixels) or pixels (Android)
77
- - iOS screenshots work without idb — only tap/swipe require it
167
+ - iOS screenshots work without idb only tap/swipe/input require it
78
168
 
79
169
  ## Using alongside React Native DevTools
80
170
 
81
171
  CDP only allows one client at a time. Switching between the MCP and DevTools is seamless — whichever connects last takes over, and the other is kicked out automatically.
82
172
 
173
+ Important practical note for agents: if a CDP-backed tool needs the runtime and `get_status` shows disconnected, the agent should try `connect` itself before asking the user to do anything. In many sessions, the MCP is only disconnected because DevTools had the socket last.
174
+
83
175
  - **To use DevTools**: just open or reconnect it. The MCP will be disconnected automatically.
84
176
  - **To return to MCP**: call `connect`. DevTools will lose its connection.
177
+ - **If a runtime tool fails due to no CDP connection**: retry after `connect`.
85
178
 
86
179
  `disconnect` is available if you want to explicitly release the connection first, but it's not required.
87
180
 
package/dist/index.js CHANGED
@@ -1,24 +1,48 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as Ze}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Ve}from"@modelcontextprotocol/sdk/server/stdio.js";import E from"ws";import me from"http";var fe=parseInt(process.env.METRO_PORT??"8081",10),ge=process.env.METRO_HOST??"localhost",he=parseInt(process.env.LOG_BUFFER_SIZE??"1000",10),ve=3e4,A=1e3,be={log:"log",info:"info",warning:"warn",warn:"warn",error:"error",debug:"debug",dir:"log",dirxml:"log",table:"log",assert:"error"};function ye(e){return e.map(n=>n.value!==void 0&&n.value!==null?String(n.value):n.description?n.description:"").filter(Boolean).join(" ")}async function Se(e,n){return new Promise(t=>{let r=me.get(`http://${e}:${n}/json/list`,i=>{let o="";i.on("data",s=>o+=s),i.on("end",()=>{try{let s=JSON.parse(o);Array.isArray(s)?t(s):t([])}catch{t([])}})});r.on("error",()=>t([])),r.setTimeout(2e3,()=>{r.destroy(),t([])})})}var R=class{buffer=[];cdpWs=null;eventsWs=null;_connected=!1;_currentTargetId=null;_lastConnectedAt=null;_totalReceived=0;_stopped=!1;_eventsBackoff=A;_deviceTitle=null;host=ge;port=fe;get connected(){return this._connected}get lastConnectedAt(){return this._lastConnectedAt}get totalReceived(){return this._totalReceived}get bufferedEntries(){return this.buffer.length}get deviceTitle(){return this._deviceTitle}start(){this._stopped=!1,this.connectEvents()}stop(){this._stopped=!0,this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this.eventsWs&&(this.eventsWs.terminate(),this.eventsWs=null)}disconnect(){this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._connected=!1,this._currentTargetId=null,this._deviceTitle=null}getEntries(n={}){let t=this.buffer;n.since!==void 0&&(t=t.filter(i=>i.timestamp>=n.since)),n.level&&(t=t.filter(i=>i.level===n.level));let r=n.lines??50;return t.slice(-r)}clearBuffer(){let n=this.buffer.length;return this.buffer=[],n}async grabConnection(){return await this.checkForNewTarget(),await new Promise(n=>setTimeout(n,500)),this._connected?`Connected to ${this._deviceTitle??"device"}.`:"No device found. Is Metro running with a connected device?"}addEntry(n){this._totalReceived++,this.buffer.push(n),this.buffer.length>he&&this.buffer.shift()}async checkForNewTarget(){let n=await Se(this.host,this.port);if(!n.length){this._connected&&(this._connected=!1,this._currentTargetId=null,this._deviceTitle=null);return}let t=n[0];t.id===this._currentTargetId&&this.cdpWs?.readyState===E.OPEN||this.connectCdp(t)}connectCdp(n){this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._currentTargetId=n.id,this._deviceTitle=n.title??null;let t=new E(n.webSocketDebuggerUrl);this.cdpWs=t,t.on("open",()=>{this._connected=!0,this._lastConnectedAt=new Date,t.send(JSON.stringify({id:1,method:"Runtime.enable",params:{}}))}),t.on("message",r=>{let i;try{i=JSON.parse(r.toString())}catch{return}i.method==="Runtime.consoleAPICalled"&&i.params&&this.handleConsoleEvent(i.params)}),t.on("close",()=>{this.cdpWs===t&&(this._connected=!1,this.cdpWs=null,this._currentTargetId=null,this._deviceTitle=null)}),t.on("error",()=>{})}handleConsoleEvent(n){let t=typeof n.type=="string"?n.type:"log",r=be[t]??"log",i=Array.isArray(n.args)?n.args:[],o=ye(i),s=typeof n.timestamp=="number"?Math.round(n.timestamp):Date.now();if(!o)return;let a=n.stackTrace?.callFrames?.filter(g=>g.url&&!g.url.startsWith("native")).map(g=>({functionName:g.functionName??"(anonymous)",url:g.url,line:g.lineNumber??0,col:g.columnNumber??0})),f=o.includes("http://")?o:void 0;this.addEntry({timestamp:s,level:r,message:o,rawMessage:f,rawFrames:a?.length?a:void 0})}connectEvents(){if(this._stopped)return;let n=`ws://${this.host}:${this.port}/events`,t;try{t=new E(n)}catch{this.scheduleEventsReconnect();return}this.eventsWs=t,t.on("open",()=>{this._eventsBackoff=A}),t.on("message",r=>{let i=null;try{i=JSON.parse(r.toString())}catch{return}(i.type==="build_failed"||i.type==="bundling_error")&&this.addEntry({timestamp:Date.now(),level:"error",message:i.message??i.type})}),t.on("close",()=>{this.eventsWs===t&&(this.eventsWs=null,this.scheduleEventsReconnect())}),t.on("error",()=>{})}scheduleEventsReconnect(){this._stopped||setTimeout(()=>{this._eventsBackoff=Math.min(this._eventsBackoff*2,ve),this.connectEvents()},this._eventsBackoff)}},c=new R;import{z as T}from"zod";var we=/\(https?:\/\/[^)]+\.bundle[^)]*:(\d+:\d+)\)/g;function S(e){return e.replace(we,"(:$1)")}function w(e){let n=new Date(e),t=String(n.getHours()).padStart(2,"0"),r=String(n.getMinutes()).padStart(2,"0"),i=String(n.getSeconds()).padStart(2,"0");return`${t}:${r}:${i}`}var N=T.object({lines:T.coerce.number().int().min(1).max(500).optional().default(50),level:T.enum(["error","warn","info","log","debug"]).optional(),since:T.string().optional()});function xe(e){let n=Number(e);if(!isNaN(n)&&n>1e9)return n<1e12?n*1e3:n;let t=e.match(/^(\d+(?:\.\d+)?)(s|m|h)$/);if(t){let r=parseFloat(t[1]),i=t[2],o={s:1e3,m:6e4,h:36e5};return Date.now()-r*o[i]}return n}function D(e){let n=e.since?xe(e.since):void 0,t=c.getEntries({lines:e.lines,level:e.level,since:n});return t.length===0?"No log entries found.":t.map(r=>`[${w(r.timestamp)}] [${r.level.toUpperCase()}] ${S(r.message)}`).join(`
3
- `)}import{z as L}from"zod";var I=L.object({lines:L.coerce.number().int().min(1).max(200).optional().default(20)});function P(e){let n=c.getEntries({level:"error",lines:e.lines});return n.length===0?"No errors in buffer.":n.map(t=>`[${w(t.timestamp)}] [ERROR]
4
- ${S(t.message)}`).join(`
2
+ import{McpServer as Lt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as zt}from"@modelcontextprotocol/sdk/server/stdio.js";import T from"ws";import He from"http";var qe=parseInt(process.env.METRO_PORT??"8081",10),Ye=process.env.METRO_HOST??"localhost",Qe=parseInt(process.env.LOG_BUFFER_SIZE??"1000",10),et=3e4,G=1e3,tt={log:"log",info:"info",warning:"warn",warn:"warn",error:"error",debug:"debug",dir:"log",dirxml:"log",table:"log",assert:"error"};function nt(e){return e.map(t=>t.value!==void 0&&t.value!==null?String(t.value):t.description?t.description:"").filter(Boolean).join(" ")}async function rt(e,t){return new Promise(n=>{let r=He.get(`http://${e}:${t}/json/list`,o=>{let i="";o.on("data",a=>i+=a),o.on("end",()=>{try{let a=JSON.parse(i);Array.isArray(a)?n(a):n([])}catch{n([])}})});r.on("error",()=>n([])),r.setTimeout(2e3,()=>{r.destroy(),n([])})})}var z=class{buffer=[];cdpWs=null;eventsWs=null;_connected=!1;_currentTargetId=null;_lastConnectedAt=null;_totalReceived=0;_stopped=!1;_eventsBackoff=G;_deviceTitle=null;_nextMessageId=1;pendingResponses=new Map;host=Ye;port=qe;get connected(){return this._connected}get lastConnectedAt(){return this._lastConnectedAt}get totalReceived(){return this._totalReceived}get bufferedEntries(){return this.buffer.length}get deviceTitle(){return this._deviceTitle}start(){this._stopped=!1,this.connectEvents()}stop(){this._stopped=!0,this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this.eventsWs&&(this.eventsWs.terminate(),this.eventsWs=null)}disconnect(){this.rejectPendingResponses(new Error("Disconnected from Metro CDP.")),this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._connected=!1,this._currentTargetId=null,this._deviceTitle=null}getEntries(t={}){let n=this.buffer;t.since!==void 0&&(n=n.filter(o=>o.timestamp>=t.since)),t.level&&(n=n.filter(o=>o.level===t.level));let r=t.lines??50;return n.slice(-r)}clearBuffer(){let t=this.buffer.length;return this.buffer=[],t}async grabConnection(){return await this.checkForNewTarget(),await new Promise(t=>setTimeout(t,500)),this._connected?`Connected to ${this._deviceTitle??"device"}.`:"No device found. Is Metro running with a connected device?"}async evaluate(t,n=5e3){if((!this._connected||!this.cdpWs||this.cdpWs.readyState!==T.OPEN)&&(await this.checkForNewTarget(),await new Promise(r=>setTimeout(r,250))),!this._connected||!this.cdpWs||this.cdpWs.readyState!==T.OPEN)throw new Error("Not connected to Metro CDP. Start Expo, make sure a device is attached, then call connect.");return this.sendCdpCommand("Runtime.evaluate",{expression:t,awaitPromise:!0,returnByValue:!0,generatePreview:!1,replMode:!0},n)}addEntry(t){this._totalReceived++,this.buffer.push(t),this.buffer.length>Qe&&this.buffer.shift()}async checkForNewTarget(){let t=await rt(this.host,this.port);if(!t.length){this._connected&&(this._connected=!1,this._currentTargetId=null,this._deviceTitle=null);return}let n=t[0];n.id===this._currentTargetId&&this.cdpWs?.readyState===T.OPEN||this.connectCdp(n)}connectCdp(t){this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._currentTargetId=t.id,this._deviceTitle=t.title??null;let n=new T(t.webSocketDebuggerUrl);this.cdpWs=n,n.on("open",()=>{this._connected=!0,this._lastConnectedAt=new Date,n.send(JSON.stringify({id:1,method:"Runtime.enable",params:{}}))}),n.on("message",r=>{let o;try{o=JSON.parse(r.toString())}catch{return}if(typeof o.id=="number"){let i=this.pendingResponses.get(o.id);if(i){clearTimeout(i.timer),this.pendingResponses.delete(o.id),i.resolve(o);return}}o.method==="Runtime.consoleAPICalled"&&o.params&&this.handleConsoleEvent(o.params)}),n.on("close",()=>{this.cdpWs===n&&(this.rejectPendingResponses(new Error("Metro CDP connection closed.")),this._connected=!1,this.cdpWs=null,this._currentTargetId=null,this._deviceTitle=null)}),n.on("error",()=>{})}sendCdpCommand(t,n,r){let o=this.cdpWs;if(!o||o.readyState!==T.OPEN)return Promise.reject(new Error("Metro CDP is not connected."));let i=this._nextMessageId++;return new Promise((a,u)=>{let l=setTimeout(()=>{this.pendingResponses.delete(i),u(new Error(`CDP command timed out after ${r}ms.`))},r);this.pendingResponses.set(i,{resolve:a,reject:u,timer:l}),o.send(JSON.stringify({id:i,method:t,params:n}),p=>{p&&(clearTimeout(l),this.pendingResponses.delete(i),u(p instanceof Error?p:new Error(String(p))))})})}rejectPendingResponses(t){for(let[n,r]of this.pendingResponses.entries())clearTimeout(r.timer),r.reject(t),this.pendingResponses.delete(n)}handleConsoleEvent(t){let n=typeof t.type=="string"?t.type:"log",r=tt[n]??"log",o=Array.isArray(t.args)?t.args:[],i=nt(o),a=typeof t.timestamp=="number"?Math.round(t.timestamp):Date.now();if(!i)return;let l=t.stackTrace?.callFrames?.filter(g=>g.url&&!g.url.startsWith("native")).map(g=>({functionName:g.functionName??"(anonymous)",url:g.url,line:g.lineNumber??0,col:g.columnNumber??0})),p=i.includes("http://")?i:void 0;this.addEntry({timestamp:a,level:r,message:i,rawMessage:p,rawFrames:l?.length?l:void 0})}connectEvents(){if(this._stopped)return;let t=`ws://${this.host}:${this.port}/events`,n;try{n=new T(t)}catch{this.scheduleEventsReconnect();return}this.eventsWs=n,n.on("open",()=>{this._eventsBackoff=G}),n.on("message",r=>{let o=null;try{o=JSON.parse(r.toString())}catch{return}(o.type==="build_failed"||o.type==="bundling_error")&&this.addEntry({timestamp:Date.now(),level:"error",message:o.message??o.type})}),n.on("close",()=>{this.eventsWs===n&&(this.eventsWs=null,this.scheduleEventsReconnect())}),n.on("error",()=>{})}scheduleEventsReconnect(){this._stopped||setTimeout(()=>{this._eventsBackoff=Math.min(this._eventsBackoff*2,et),this.connectEvents()},this._eventsBackoff)}},c=new z;import{z as I}from"zod";var ot=/\(https?:\/\/[^)]+\.bundle[^)]*:(\d+:\d+)\)/g;function E(e){return e.replace(ot,"(:$1)")}function P(e){let t=new Date(e),n=String(t.getHours()).padStart(2,"0"),r=String(t.getMinutes()).padStart(2,"0"),o=String(t.getSeconds()).padStart(2,"0");return`${n}:${r}:${o}`}var U=I.object({lines:I.coerce.number().int().min(1).max(500).optional().default(50),level:I.enum(["error","warn","info","log","debug"]).optional(),since:I.string().optional()});function it(e){let t=Number(e);if(!isNaN(t)&&t>1e9)return t<1e12?t*1e3:t;let n=e.match(/^(\d+(?:\.\d+)?)(s|m|h)$/);if(n){let r=parseFloat(n[1]),o=n[2],i={s:1e3,m:6e4,h:36e5};return Date.now()-r*i[o]}return t}function V(e){let t=e.since?it(e.since):void 0,n=c.getEntries({lines:e.lines,level:e.level,since:t});return n.length===0?"No log entries found.":n.map(r=>`[${P(r.timestamp)}] [${r.level.toUpperCase()}] ${E(r.message)}`).join(`
3
+ `)}import{z as X}from"zod";var Z=X.object({lines:X.coerce.number().int().min(1).max(200).optional().default(20)});function H(e){let t=c.getEntries({level:"error",lines:e.lines});return t.length===0?"No errors in buffer.":t.map(n=>`[${P(n.timestamp)}] [ERROR]
4
+ ${E(n.message)}`).join(`
5
5
 
6
6
  ---
7
7
 
8
- `)}function F(){let e={connected:c.connected,host:c.host,port:c.port,device:c.deviceTitle,buffered_entries:c.bufferedEntries,last_connected_at:c.lastConnectedAt?.toISOString()??null,total_received:c.totalReceived,expo_sdk_version:null};return JSON.stringify(e,null,2)}function W(){let e=c.clearBuffer();return`Cleared ${e} log ${e===1?"entry":"entries"} from the buffer.`}import{z as M}from"zod";var B=M.object({duration:M.string().optional().default("10s"),level:M.enum(["error","warn","info","log","debug"]).optional()});function $e(e){let n=e.match(/^(\d+(?:\.\d+)?)(s|m)$/);if(!n)return 1e4;let t=parseFloat(n[1]),i=n[2]==="m"?t*6e4:t*1e3;return Math.min(i,3e4)}var _e=500;async function j(e){if(!c.connected)return"Metro is not connected. Start Expo dev server and try again.";let n=$e(e.duration),t=c.bufferedEntries,r=Date.now(),i=e.level;await new Promise(a=>{let f=setInterval(()=>{Date.now()-r>=n&&(clearInterval(f),a())},_e)});let s=c.getEntries({lines:c.bufferedEntries}).slice(t),l=i?s.filter(a=>a.level===i):s;return l.length===0?`No logs received during ${e.duration} window.`:l.map(a=>`[${w(a.timestamp)}] [${a.level.toUpperCase()}] ${S(a.message)}`).join(`
9
- `)}import Te from"http";var ke=parseInt(process.env.METRO_PORT??"8081",10),Ee=process.env.METRO_HOST??"localhost";async function U(){return new Promise(e=>{let n=Te.request({hostname:Ee,port:ke,path:"/reload",method:"POST"},t=>{t.resume(),t.statusCode===200?e("App reloaded."):e(`Reload failed: HTTP ${t.statusCode}`)});n.on("error",t=>e(`Reload failed: ${t.message}`)),n.setTimeout(3e3,()=>{n.destroy(),e("Reload failed: timeout")}),n.end()})}import{z}from"zod";import Re from"http";import{SourceMapConsumer as Me}from"source-map";var G=z.object({message:z.string().optional()}),k=new Map,Oe=6e4;function H(e,n,t){return e.replace(/^https?:\/\/[^/]+/,`http://${n}:${t}`)}function K(e){let n=e.match(/(?:\/\/&|\?)(.+)$/),t=n?n[1]:"dev=true&minify=false";return`${e.replace(/(\/[^/?]+)\.bundle.*$/,"$1.map")}?${t}`}async function Ce(e){let n=k.get(e);return n&&Date.now()-n.fetchedAt<Oe?n.consumer:new Promise(t=>{let r=new URL(e),i=Re.get({hostname:r.hostname,port:Number(r.port)||8081,path:r.pathname+r.search},o=>{let s=[];o.on("data",l=>s.push(l)),o.on("end",async()=>{if(o.statusCode!==200){t(null);return}try{let l=JSON.parse(Buffer.concat(s).toString()),a=await Me.with(l,null,f=>f);k.set(e,{consumer:a,fetchedAt:Date.now()}),t(a)}catch{t(null)}})});i.on("error",()=>t(null)),i.setTimeout(1e4,()=>{i.destroy(),t(null)})})}function q(){k.forEach(e=>e.consumer.destroy()),k.clear()}function Ae(e){let n=/at\s+([\w$.<>[\] ]+?)\s+\((https?:\/\/[^)]+\.bundle[^)]*):(\d+):(\d+)\)/g,t=[],r;for(;(r=n.exec(e))!==null;)t.push({functionName:r[1].trim(),url:r[2],line:parseInt(r[3])-1,col:parseInt(r[4])});return t}async function Ne(e,n){let t=[],r=new Map;for(let o of e){let s=H(o.url,c.host,c.port),l=K(s);r.has(l)||r.set(l,[]),r.get(l).push(o)}let i=new Map;await Promise.all([...r.keys()].map(async o=>{i.set(o,await Ce(o))}));for(let o of e){let s=H(o.url,c.host,c.port),l=K(s),a=i.get(l)??null;if(a){let f=a.originalPositionFor({line:o.line+1,column:o.col});if(f.source){let g=f.source.replace(/^.*\/\/\//,"").replace(/\?.*$/,"");t.push(` at ${o.functionName} (${g}:${f.line}:${f.column})`);continue}}t.push(` at ${o.functionName} (:${o.line+1}:${o.col})`)}return t}async function J(e){let n=c.getEntries({level:"error",lines:50}),t=e.message?[...n].reverse().find(a=>a.message.includes(e.message)):n.at(-1);if(!t)return"No error entries in buffer.";let r=`[ERROR] ${t.message.split(`
10
- `)[0]}`;if(!t.rawMessage)return`${r}
8
+ `)}function q(){let e={connected:c.connected,host:c.host,port:c.port,device:c.deviceTitle,buffered_entries:c.bufferedEntries,last_connected_at:c.lastConnectedAt?.toISOString()??null,total_received:c.totalReceived,expo_sdk_version:null};return JSON.stringify(e,null,2)}function Y(){let e=c.clearBuffer();return`Cleared ${e} log ${e===1?"entry":"entries"} from the buffer.`}import{z as W}from"zod";var Q=W.object({duration:W.string().optional().default("10s"),level:W.enum(["error","warn","info","log","debug"]).optional()});function st(e){let t=e.match(/^(\d+(?:\.\d+)?)(s|m)$/);if(!t)return 1e4;let n=parseFloat(t[1]),o=t[2]==="m"?n*6e4:n*1e3;return Math.min(o,3e4)}var at=500;async function ee(e){if(!c.connected)return"Metro is not connected. Start Expo dev server and try again.";let t=st(e.duration),n=c.bufferedEntries,r=Date.now(),o=e.level;await new Promise(l=>{let p=setInterval(()=>{Date.now()-r>=t&&(clearInterval(p),l())},at)});let a=c.getEntries({lines:c.bufferedEntries}).slice(n),u=o?a.filter(l=>l.level===o):a;return u.length===0?`No logs received during ${e.duration} window.`:u.map(l=>`[${P(l.timestamp)}] [${l.level.toUpperCase()}] ${E(l.message)}`).join(`
9
+ `)}import ct from"http";var ut=parseInt(process.env.METRO_PORT??"8081",10),lt=process.env.METRO_HOST??"localhost";async function te(){return new Promise(e=>{let t=ct.request({hostname:lt,port:ut,path:"/reload",method:"POST"},n=>{n.resume(),n.statusCode===200?e("App reloaded."):e(`Reload failed: HTTP ${n.statusCode}`)});t.on("error",n=>e(`Reload failed: ${n.message}`)),t.setTimeout(3e3,()=>{t.destroy(),e("Reload failed: timeout")}),t.end()})}import{z as ne}from"zod";import mt from"http";import{SourceMapConsumer as pt}from"source-map";var ie=ne.object({message:ne.string().optional()}),j=new Map,dt=6e4;function re(e,t,n){return e.replace(/^https?:\/\/[^/]+/,`http://${t}:${n}`)}function oe(e){let t=e.match(/(?:\/\/&|\?)(.+)$/),n=t?t[1]:"dev=true&minify=false";return`${e.replace(/(\/[^/?]+)\.bundle.*$/,"$1.map")}?${n}`}async function ft(e){let t=j.get(e);return t&&Date.now()-t.fetchedAt<dt?t.consumer:new Promise(n=>{let r=new URL(e),o=mt.get({hostname:r.hostname,port:Number(r.port)||8081,path:r.pathname+r.search},i=>{let a=[];i.on("data",u=>a.push(u)),i.on("end",async()=>{if(i.statusCode!==200){n(null);return}try{let u=JSON.parse(Buffer.concat(a).toString()),l=await pt.with(u,null,p=>p);j.set(e,{consumer:l,fetchedAt:Date.now()}),n(l)}catch{n(null)}})});o.on("error",()=>n(null)),o.setTimeout(1e4,()=>{o.destroy(),n(null)})})}function se(){j.forEach(e=>e.consumer.destroy()),j.clear()}function gt(e){let t=/at\s+([\w$.<>[\] ]+?)\s+\((https?:\/\/[^)]+\.bundle[^)]*):(\d+):(\d+)\)/g,n=[],r;for(;(r=t.exec(e))!==null;)n.push({functionName:r[1].trim(),url:r[2],line:parseInt(r[3])-1,col:parseInt(r[4])});return n}async function ht(e,t){let n=[],r=new Map;for(let i of e){let a=re(i.url,c.host,c.port),u=oe(a);r.has(u)||r.set(u,[]),r.get(u).push(i)}let o=new Map;await Promise.all([...r.keys()].map(async i=>{o.set(i,await ft(i))}));for(let i of e){let a=re(i.url,c.host,c.port),u=oe(a),l=o.get(u)??null;if(l){let p=l.originalPositionFor({line:i.line+1,column:i.col});if(p.source){let g=p.source.replace(/^.*\/\/\//,"").replace(/\?.*$/,"");n.push(` at ${i.functionName} (${g}:${p.line}:${p.column})`);continue}}n.push(` at ${i.functionName} (:${i.line+1}:${i.col})`)}return n}async function ae(e){let t=c.getEntries({level:"error",lines:50}),n=e.message?[...t].reverse().find(l=>l.message.includes(e.message)):t.at(-1);if(!n)return"No error entries in buffer.";let r=`[ERROR] ${n.message.split(`
10
+ `)[0]}`;if(!n.rawMessage)return`${r}
11
11
 
12
- No stack frames available.`;let i=Ae(t.rawMessage);if(!i.length)return`${r}
12
+ No stack frames available.`;let o=gt(n.rawMessage);if(!o.length)return`${r}
13
13
 
14
- No stack frames available.`;let o=await Ne(i),s=o.filter(a=>!a.includes("node_modules")&&!a.includes("(:")),l=[r,""];return s.length?l.push(...s):l.push(...o),l.join(`
15
- `)}import{z as O}from"zod";import{execSync as b}from"child_process";import*as y from"fs";import*as V from"os";import*as Q from"path";import{execSync as Y}from"child_process";function X(e){try{return Y(e,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return""}}function Z(e){try{return Y(`which ${e}`,{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function De(){if(!Z("xcrun"))return[];let e=X("xcrun simctl list devices booted --json");if(!e)return[];try{let n=JSON.parse(e),t=[];for(let[,r]of Object.entries(n.devices))for(let i of r)i.state==="Booted"&&t.push({id:i.udid,name:i.name,platform:"ios"});return t}catch{return[]}}function Le(){if(!Z("adb"))return[];let e=X("adb devices -l");if(!e)return[];let n=[],t=e.split(`
16
- `).slice(1);for(let r of t){let i=r.trim().split(/\s+/);if(i.length<2||i[1]!=="device")continue;let o=i[0],s=r.match(/model:(\S+)/),l=s?s[1].replace(/_/g," "):o;n.push({id:o,name:l,platform:"android"})}return n}function d(){return[...De(),...Le()]}function h(e,n){let t=d();return t.length?n?t.find(r=>r.id===n||r.name.toLowerCase().includes(n.toLowerCase()))??null:e==="ios"?t.find(r=>r.platform==="ios")??null:e==="android"?t.find(r=>r.platform==="android")??null:t.find(r=>r.platform==="ios")??t[0]:null}var ee=O.object({device_id:O.string().optional(),platform:O.enum(["ios","android"]).optional()});function Ie(e){try{let n=b(`sips -g pixelWidth -g pixelHeight "${e}"`,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString(),t=n.match(/pixelWidth:\s*(\d+)/),r=n.match(/pixelHeight:\s*(\d+)/);if(!t||!r)return 1;let i=parseInt(t[1]),o=parseInt(r[1]),s=Math.max(i,o);return s>=2500&&i%3===0||s>=2500?3:s>=1334?2:1}catch{return 1}}function Pe(e,n){b(`xcrun simctl io "${e}" screenshot "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});let t=Ie(n);if(t>1)try{let r=b(`sips -g pixelWidth -g pixelHeight "${n}"`,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString(),i=r.match(/pixelWidth:\s*(\d+)/),o=r.match(/pixelHeight:\s*(\d+)/);if(i&&o){let s=Math.round(parseInt(i[1])/t),l=Math.round(parseInt(o[1])/t),a=n.replace(".png","-points.png");b(`sips -z ${l} ${s} "${n}" --out "${a}"`,{timeout:1e4,stdio:"ignore"}),y.renameSync(a,n)}}catch{}}function Fe(e,n){let t=`/sdcard/mcp_screenshot_${Date.now()}.png`;b(`adb -s "${e}" shell screencap -p "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),b(`adb -s "${e}" pull "${t}" "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),b(`adb -s "${e}" shell rm "${t}"`,{timeout:5e3,stdio:"ignore"})}function We(e){if(e.length<24||e.readUInt32BE(0)!==2303741511)return null;let n=e.readUInt32BE(16),t=e.readUInt32BE(20);return{width:n,height:t}}function te(e){let n=d();if(!n.length)return{type:"text",text:"No active simulators or emulators found. Start a simulator (iOS) or emulator (Android) first."};let t=h(e.platform,e.device_id);if(!t)return{type:"text",text:`No matching device found. Available: ${n.map(i=>`${i.name} (${i.platform})`).join(", ")}`};let r=Q.join(V.tmpdir(),`expo-mcp-screenshot-${Date.now()}.png`);try{t.platform==="ios"?Pe(t.id,r):Fe(t.id,r);let i=y.readFileSync(r),o=i.toString("base64"),s=We(i);return y.unlinkSync(r),{type:"image",data:o,mimeType:"image/png",width:s?.width??0,height:s?.height??0}}catch(i){try{y.unlinkSync(r)}catch{}return{type:"text",text:`Screenshot failed: ${i instanceof Error?i.message:String(i)}`}}}import{z as p}from"zod";import{execSync as $}from"child_process";import{spawn as Be}from"child_process";var m=null,C=null;function x(e){m&&!m.killed&&C===e||(m&&!m.killed&&m.kill(),m=Be("idb_companion",["--udid",e],{detached:!1,stdio:"ignore"}),C=e,m.on("exit",()=>{m=null,C=null}),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,800))}process.on("exit",()=>m?.kill());process.on("SIGTERM",()=>{m?.kill(),process.exit(0)});process.on("SIGINT",()=>{m?.kill(),process.exit(0)});var ne=p.object({x:p.number().int().describe("X coordinate in points/pixels"),y:p.number().int().describe("Y coordinate in points/pixels"),device_id:p.string().optional(),platform:p.enum(["ios","android"]).optional()}),re=p.object({x1:p.number().int(),y1:p.number().int(),x2:p.number().int(),y2:p.number().int(),duration_ms:p.number().int().min(50).max(5e3).optional().default(300),device_id:p.string().optional(),platform:p.enum(["ios","android"]).optional()});function ie(){try{return $("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function je(e,n,t){if(ie()){x(e),$(`idb ui tap ${n} ${t} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}let r=JSON.stringify({touches:[{x:n,y:t,action:"began"}]});try{$(`echo '${r}' | xcrun simctl io "${e}" sendtouchJSON -`,{timeout:5e3,stdio:["pipe","ignore","pipe"]});return}catch{}throw new Error(`iOS tap requires idb (Facebook iOS Development Bridge).
14
+ No stack frames available.`;let i=await ht(o),a=i.filter(l=>!l.includes("node_modules")&&!l.includes("(:")),u=[r,""];return a.length?u.push(...a):u.push(...i),u.join(`
15
+ `)}import{z as J}from"zod";import{execSync as x}from"child_process";import*as S from"fs";import*as me from"os";import*as pe from"path";import{execSync as ce}from"child_process";function ue(e){try{return ce(e,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return""}}function le(e){try{return ce(`which ${e}`,{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function vt(){if(!le("xcrun"))return[];let e=ue("xcrun simctl list devices booted --json");if(!e)return[];try{let t=JSON.parse(e),n=[];for(let[,r]of Object.entries(t.devices))for(let o of r)o.state==="Booted"&&n.push({id:o.udid,name:o.name,platform:"ios"});return n}catch{return[]}}function yt(){if(!le("adb"))return[];let e=ue("adb devices -l");if(!e)return[];let t=[],n=e.split(`
16
+ `).slice(1);for(let r of n){let o=r.trim().split(/\s+/);if(o.length<2||o[1]!=="device")continue;let i=o[0],a=r.match(/model:(\S+)/),u=a?a[1].replace(/_/g," "):i;t.push({id:i,name:u,platform:"android"})}return t}function h(){return[...vt(),...yt()]}function k(e,t){let n=h();return n.length?t?n.find(r=>r.id===t||r.name.toLowerCase().includes(t.toLowerCase()))??null:e==="ios"?n.find(r=>r.platform==="ios")??null:e==="android"?n.find(r=>r.platform==="android")??null:n.find(r=>r.platform==="ios")??n[0]:null}var de=J.object({device_id:J.string().optional(),platform:J.enum(["ios","android"]).optional()});function bt(e){try{let t=x(`sips -g pixelWidth -g pixelHeight "${e}"`,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString(),n=t.match(/pixelWidth:\s*(\d+)/),r=t.match(/pixelHeight:\s*(\d+)/);if(!n||!r)return 1;let o=parseInt(n[1]),i=parseInt(r[1]),a=Math.max(o,i);return a>=2500&&o%3===0||a>=2500?3:a>=1334?2:1}catch{return 1}}function _t(e,t){x(`xcrun simctl io "${e}" screenshot "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});let n=bt(t);if(n>1)try{let r=x(`sips -g pixelWidth -g pixelHeight "${t}"`,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString(),o=r.match(/pixelWidth:\s*(\d+)/),i=r.match(/pixelHeight:\s*(\d+)/);if(o&&i){let a=Math.round(parseInt(o[1])/n),u=Math.round(parseInt(i[1])/n),l=t.replace(".png","-points.png");x(`sips -z ${u} ${a} "${t}" --out "${l}"`,{timeout:1e4,stdio:"ignore"}),S.renameSync(l,t)}}catch{}}function kt(e,t){let n=`/sdcard/mcp_screenshot_${Date.now()}.png`;x(`adb -s "${e}" shell screencap -p "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),x(`adb -s "${e}" pull "${n}" "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),x(`adb -s "${e}" shell rm "${n}"`,{timeout:5e3,stdio:"ignore"})}function wt(e){if(e.length<24||e.readUInt32BE(0)!==2303741511)return null;let t=e.readUInt32BE(16),n=e.readUInt32BE(20);return{width:t,height:n}}function fe(e){let t=h();if(!t.length)return{type:"text",text:"No active simulators or emulators found. Start a simulator (iOS) or emulator (Android) first."};let n=k(e.platform,e.device_id);if(!n)return{type:"text",text:`No matching device found. Available: ${t.map(o=>`${o.name} (${o.platform})`).join(", ")}`};let r=pe.join(me.tmpdir(),`expo-mcp-screenshot-${Date.now()}.png`);try{n.platform==="ios"?_t(n.id,r):kt(n.id,r);let o=S.readFileSync(r),i=o.toString("base64"),a=wt(o);return S.unlinkSync(r),{type:"image",data:i,mimeType:"image/png",width:a?.width??0,height:a?.height??0}}catch(o){try{S.unlinkSync(r)}catch{}return{type:"text",text:`Screenshot failed: ${o instanceof Error?o.message:String(o)}`}}}import{z as f}from"zod";import{execSync as O}from"child_process";import{spawn as xt}from"child_process";var v=null,K=null;function R(e){v&&!v.killed&&K===e||(v&&!v.killed&&v.kill(),v=xt("idb_companion",["--udid",e],{detached:!1,stdio:"ignore"}),K=e,v.on("exit",()=>{v=null,K=null}),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,800))}process.on("exit",()=>v?.kill());process.on("SIGTERM",()=>{v?.kill(),process.exit(0)});process.on("SIGINT",()=>{v?.kill(),process.exit(0)});var ge=f.object({x:f.number().int().describe("X coordinate in points/pixels"),y:f.number().int().describe("Y coordinate in points/pixels"),device_id:f.string().optional(),platform:f.enum(["ios","android"]).optional()}),he=f.object({x1:f.number().int(),y1:f.number().int(),x2:f.number().int(),y2:f.number().int(),duration_ms:f.number().int().min(50).max(5e3).optional().default(300),device_id:f.string().optional(),platform:f.enum(["ios","android"]).optional()});function ve(){try{return O("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function St(e,t,n){if(ve()){R(e),O(`idb ui tap ${t} ${n} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}let r=JSON.stringify({touches:[{x:t,y:n,action:"began"}]});try{O(`echo '${r}' | xcrun simctl io "${e}" sendtouchJSON -`,{timeout:5e3,stdio:["pipe","ignore","pipe"]});return}catch{}throw new Error(`iOS tap requires idb (Facebook iOS Development Bridge).
17
17
  Install via: brew install idb-companion && pip3 install fb-idb
18
- Or: brew install facebook/fb/idb-companion`)}function Ue(e,n,t){$(`adb -s "${e}" shell input tap ${n} ${t}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function ze(e,n,t,r,i,o){if(ie()){x(e);let s=(o/1e3).toFixed(2);$(`idb ui swipe ${n} ${t} ${r} ${i} ${s} --udid "${e}"`,{timeout:o+5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS swipe requires idb (Facebook iOS Development Bridge).
19
- Install via: brew install idb-companion && pip3 install fb-idb`)}function He(e,n,t,r,i,o){$(`adb -s "${e}" shell input swipe ${n} ${t} ${r} ${i} ${o}`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function oe(e){let n=d();if(!n.length)return"No active simulators or emulators found.";let t=h(e.platform,e.device_id);if(!t)return`No matching device found. Available: ${n.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return t.platform==="ios"?je(t.id,e.x,e.y):Ue(t.id,e.x,e.y),`Tapped (${e.x}, ${e.y}) on ${t.name} [${t.platform}].`}catch(r){return`Tap failed: ${r instanceof Error?r.message:String(r)}`}}function se(e){let n=d();if(!n.length)return"No active simulators or emulators found.";let t=h(e.platform,e.device_id);if(!t)return`No matching device found. Available: ${n.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return t.platform==="ios"?(ze(t.id,e.x1,e.y1,e.x2,e.y2,e.duration_ms),`Swiped from (${e.x1}, ${e.y1}) to (${e.x2}, ${e.y2}) on ${t.name} [ios]. Note: iOS swipe simulation is limited \u2014 use Android for reliable swipe gestures.`):(He(t.id,e.x1,e.y1,e.x2,e.y2,e.duration_ms),`Swiped from (${e.x1}, ${e.y1}) to (${e.x2}, ${e.y2}) on ${t.name} [android] over ${e.duration_ms}ms.`)}catch(r){return`Swipe failed: ${r instanceof Error?r.message:String(r)}`}}import{z as v}from"zod";import{execSync as _}from"child_process";var ce=v.object({text:v.string().describe("Text to type into the focused input field"),device_id:v.string().optional(),platform:v.enum(["ios","android"]).optional()}),ae=v.object({key:v.enum(["enter","backspace","delete","tab","escape","home","end","back","space","up","down","left","right"]).describe("Key to press"),device_id:v.string().optional(),platform:v.enum(["ios","android"]).optional()}),Ke={enter:66,backspace:67,delete:67,tab:61,escape:111,home:3,end:123,back:4,space:62,up:19,down:20,left:21,right:22},Ge={enter:40,backspace:42,delete:76,tab:43,escape:41,home:74,end:77,back:41,space:44,up:82,down:81,left:80,right:79};function le(){try{return _("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function qe(e,n){if(le()){x(e);let t=n.replace(/'/g,`'"'"'`);_(`idb ui text '${t}' --udid "${e}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS text input requires idb.
20
- Install via: brew install idb-companion && pip3 install fb-idb`)}function Je(e,n){let t=n.replace(/\\/g,"\\\\").replace(/ /g,"%s").replace(/'/g,"\\'").replace(/"/g,'\\"');_(`adb -s "${e}" shell input text "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function Ye(e,n){if(le()){x(e);let t=Ge[n];if(t===void 0)throw new Error(`Unknown key: ${n}`);_(`idb ui key ${t} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS key input requires idb.
21
- Install via: brew install idb-companion && pip3 install fb-idb`)}function Xe(e,n){let t=Ke[n];if(t===void 0)throw new Error(`Unknown key: ${n}`);_(`adb -s "${e}" shell input keyevent ${t}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function ue(e){let n=d();if(!n.length)return"No active simulators or emulators found.";let t=h(e.platform,e.device_id);if(!t)return`No matching device found. Available: ${n.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return t.platform==="ios"?qe(t.id,e.text):Je(t.id,e.text),`Typed "${e.text}" on ${t.name} [${t.platform}].`}catch(r){return`input_text failed: ${r instanceof Error?r.message:String(r)}`}}function pe(e){let n=d();if(!n.length)return"No active simulators or emulators found.";let t=h(e.platform,e.device_id);if(!t)return`No matching device found. Available: ${n.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return t.platform==="ios"?Ye(t.id,e.key):Xe(t.id,e.key),`Key "${e.key}" sent to ${t.name} [${t.platform}].`}catch(r){return`input_key failed: ${r instanceof Error?r.message:String(r)}`}}function de(){let e=d();if(!e.length)return"No active simulators or emulators found.\n\n- iOS: start a simulator via Xcode or `xcrun simctl boot <device>`\n- Android: start an emulator via Android Studio or `emulator -avd <name>`";let n=e.map(t=>`- ${t.name} [${t.platform}] id: ${t.id}`);return`Active devices (${e.length}):
22
- ${n.join(`
23
- `)}`}var u=new Ze({name:"expo-metro-mcp",version:"0.1.0"});u.registerTool("get_logs",{description:"Fetch recent logs from the Metro dev server buffer. Supports filtering by level and time.",inputSchema:N.shape},async e=>({content:[{type:"text",text:D(e)}]}));u.registerTool("get_errors",{description:"Fetch recent errors from the Metro dev server buffer, with stack traces.",inputSchema:I.shape},async e=>({content:[{type:"text",text:P(e)}]}));u.registerTool("get_status",{description:"Check the connection status of the Metro dev server and buffer statistics."},async()=>({content:[{type:"text",text:F()}]}));u.registerTool("connect",{description:"Grab the CDP connection to the Metro dev server. Use this if get_status shows disconnected, or to take over the connection from React Native DevTools."},async()=>({content:[{type:"text",text:await c.grabConnection()}]}));u.registerTool("disconnect",{description:"Release the CDP connection so React Native DevTools can connect. Use this before switching to DevTools. Call connect when you want to reattach."},async()=>(c.disconnect(),{content:[{type:"text",text:"Disconnected. DevTools can now connect freely."}]}));u.registerTool("clear_logs",{description:"Clear the internal log buffer. Useful after resolving an issue."},async()=>({content:[{type:"text",text:W()}]}));u.registerTool("watch_logs",{description:"Listen for incoming logs for a short time window and return all collected entries.",inputSchema:B.shape},async e=>({content:[{type:"text",text:await j(e)}]}));u.registerTool("reload",{description:"Reload the React Native app via Metro."},async()=>(q(),{content:[{type:"text",text:await U()}]}));u.registerTool("resolve_stack",{description:"Resolve a stack trace from the buffer against the Metro source map, showing original file:line instead of bundle offsets. Optionally filter by error message substring.",inputSchema:G.shape},async e=>({content:[{type:"text",text:await J(e)}]}));u.registerTool("list_devices",{description:"List active iOS simulators and Android emulators. Use this to find available devices before taking screenshots or sending taps."},async()=>({content:[{type:"text",text:de()}]}));u.registerTool("screenshot",{description:"Take a screenshot of the active iOS simulator or Android emulator. Returns the image directly. Optionally specify platform ('ios' or 'android') or device_id if multiple devices are running.",inputSchema:ee.shape},async e=>{let n=te(e);if(n.type==="image"){let t=n.width&&n.height?`Screenshot dimensions: ${n.width}x${n.height}px. Use these exact coordinates for tap and swipe \u2014 no scaling needed.`:"Screenshot dimensions unknown.";return{content:[{type:"image",data:n.data,mimeType:n.mimeType},{type:"text",text:t}]}}return{content:[{type:"text",text:n.text}]}});u.registerTool("tap",{description:"Tap at x,y coordinates on the active simulator/emulator. Use screenshot first to determine coordinates. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Optionally specify platform or device_id.",inputSchema:ne.shape},async e=>({content:[{type:"text",text:oe(e)}]}));u.registerTool("swipe",{description:"Swipe from one coordinate to another. Requires idb on iOS (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Useful for scrolling lists or dismissing sheets.",inputSchema:re.shape},async e=>({content:[{type:"text",text:se(e)}]}));u.registerTool("input_text",{description:"Type text into the currently focused input field on the active simulator/emulator. Use tap to focus an input first, then call this tool. Works without requiring the on-screen keyboard to be visible. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box.",inputSchema:ce.shape},async e=>({content:[{type:"text",text:ue(e)}]}));u.registerTool("input_key",{description:"Send a special key press to the active simulator/emulator. Supported keys: enter, backspace, delete, tab, escape, home, end, back, space, up, down, left, right. Use after input_text to submit forms (enter) or correct mistakes (backspace).",inputSchema:ae.shape},async e=>({content:[{type:"text",text:pe(e)}]}));async function Qe(){c.start();let e=new Ve;await u.connect(e),process.on("SIGINT",()=>{c.stop(),process.exit(0)}),process.on("SIGTERM",()=>{c.stop(),process.exit(0)})}Qe().catch(e=>{process.stderr.write(`Fatal error: ${e}
18
+ Or: brew install facebook/fb/idb-companion`)}function Mt(e,t,n){O(`adb -s "${e}" shell input tap ${t} ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function $t(e,t,n,r,o,i){if(ve()){R(e);let a=(i/1e3).toFixed(2);O(`idb ui swipe ${t} ${n} ${r} ${o} ${a} --udid "${e}"`,{timeout:i+5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS swipe requires idb (Facebook iOS Development Bridge).
19
+ Install via: brew install idb-companion && pip3 install fb-idb`)}function Tt(e,t,n,r,o,i){O(`adb -s "${e}" shell input swipe ${t} ${n} ${r} ${o} ${i}`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function ye(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?St(n.id,e.x,e.y):Mt(n.id,e.x,e.y),`Tapped (${e.x}, ${e.y}) on ${n.name} [${n.platform}].`}catch(r){return`Tap failed: ${r instanceof Error?r.message:String(r)}`}}function be(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?($t(n.id,e.x1,e.y1,e.x2,e.y2,e.duration_ms),`Swiped from (${e.x1}, ${e.y1}) to (${e.x2}, ${e.y2}) on ${n.name} [ios]. Note: iOS swipe simulation is limited \u2014 use Android for reliable swipe gestures.`):(Tt(n.id,e.x1,e.y1,e.x2,e.y2,e.duration_ms),`Swiped from (${e.x1}, ${e.y1}) to (${e.x2}, ${e.y2}) on ${n.name} [android] over ${e.duration_ms}ms.`)}catch(r){return`Swipe failed: ${r instanceof Error?r.message:String(r)}`}}import{z as w}from"zod";import{execSync as C}from"child_process";var _e=w.object({text:w.string().describe("Text to type into the focused input field"),device_id:w.string().optional(),platform:w.enum(["ios","android"]).optional()}),ke=w.object({key:w.enum(["enter","backspace","delete","tab","escape","home","end","back","space","up","down","left","right"]).describe("Key to press"),device_id:w.string().optional(),platform:w.enum(["ios","android"]).optional()}),Et={enter:66,backspace:67,delete:67,tab:61,escape:111,home:3,end:123,back:4,space:62,up:19,down:20,left:21,right:22},Pt={enter:40,backspace:42,delete:76,tab:43,escape:41,home:74,end:77,back:41,space:44,up:82,down:81,left:80,right:79};function we(){try{return C("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function Rt(e,t){if(we()){R(e);let n=t.replace(/'/g,`'"'"'`);C(`idb ui text '${n}' --udid "${e}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS text input requires idb.
20
+ Install via: brew install idb-companion && pip3 install fb-idb`)}function Ot(e,t){let n=t.replace(/\\/g,"\\\\").replace(/ /g,"%s").replace(/'/g,"\\'").replace(/"/g,'\\"');C(`adb -s "${e}" shell input text "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function Ct(e,t){if(we()){R(e);let n=Pt[t];if(n===void 0)throw new Error(`Unknown key: ${t}`);C(`idb ui key ${n} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS key input requires idb.
21
+ Install via: brew install idb-companion && pip3 install fb-idb`)}function At(e,t){let n=Et[t];if(n===void 0)throw new Error(`Unknown key: ${t}`);C(`adb -s "${e}" shell input keyevent ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function xe(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?Rt(n.id,e.text):Ot(n.id,e.text),`Typed "${e.text}" on ${n.name} [${n.platform}].`}catch(r){return`input_text failed: ${r instanceof Error?r.message:String(r)}`}}function Se(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?Ct(n.id,e.key):At(n.id,e.key),`Key "${e.key}" sent to ${n.name} [${n.platform}].`}catch(r){return`input_key failed: ${r instanceof Error?r.message:String(r)}`}}function Me(){let e=h();if(!e.length)return"No active simulators or emulators found.\n\n- iOS: start a simulator via Xcode or `xcrun simctl boot <device>`\n- Android: start an emulator via Android Studio or `emulator -avd <name>`";let t=e.map(n=>`- ${n.name} [${n.platform}] id: ${n.id}`);return`Active devices (${e.length}):
22
+ ${t.join(`
23
+ `)}`}import{z as F}from"zod";var Te=F.object({code:F.string().min(1).max(2e4),timeout_ms:F.coerce.number().int().min(100).max(3e4).optional().default(5e3)});function $e(e){if(e===void 0)return"undefined";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean"||e===null)return String(e);try{return JSON.stringify(e,null,2)}catch{return String(e)}}function Nt(e){if(!e)return"Unknown runtime error.";let t=typeof e.text=="string"?e.text:"Runtime evaluation failed.",n=e.exception&&typeof e.exception=="object"?e.exception:void 0,r=typeof n?.description=="string"?n.description:typeof n?.value=="string"?n.value:void 0;return r?`${t}
24
+ ${r}`:t}async function Ee(e){let t=await c.evaluate(e.code,e.timeout_ms);if(t.error){let B=[t.error.message??"CDP error"];return t.error.data!==void 0&&B.push($e(t.error.data)),`Evaluation failed.
25
+ ${B.join(`
26
+ `)}`}let n=t.result&&typeof t.result=="object"?t.result:void 0,r=n?.exceptionDetails&&typeof n.exceptionDetails=="object"?n.exceptionDetails:void 0;if(r)return`Evaluation threw.
27
+ ${Nt(r)}`;let o=n?.result&&typeof n.result=="object"?n.result:void 0;if(!o)return"Evaluation completed, but no result was returned.";let i=typeof o.type=="string"?o.type:"unknown",a=typeof o.subtype=="string"?o.subtype:void 0,u=o.value,l=typeof o.description=="string"?o.description:void 0,p=[`type: ${i}`];a&&p.push(`subtype: ${a}`);let g=u!==void 0?$e(u):l??"(no serializable value)";return`${p.join(", ")}
28
+ ${g}`}var Dt=new Set(["1","true","yes","on"]);function Pe(e,t=!1){let n=process.env[e];return n==null?t:Dt.has(n.trim().toLowerCase())}function It(e){let t=process.env[e];return t?t.split(",").map(n=>n.trim()).filter(Boolean):[]}var A={readOnly:Pe("EXPO_METRO_MCP_READ_ONLY"),enableEval:Pe("EXPO_METRO_MCP_ENABLE_EVAL"),mmkvPrefixAllowlist:It("EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST")};function M(e){if(A.readOnly)throw new Error(`${e} is disabled because EXPO_METRO_MCP_READ_ONLY=1.`)}function y(e){let{mmkvPrefixAllowlist:t}=A;if(t.length&&!t.some(n=>e.startsWith(n)))throw new Error(`MMKV key "${e}" is not allowed by EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST.`)}function Re(e){let{mmkvPrefixAllowlist:t}=A;return t.length?e.filter(n=>t.some(r=>n.startsWith(r))):e}import{z as s}from"zod";var jt="globalThis.__EXPO_METRO_MCP__?.mmkv",Oe=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ce=s.object({key:s.string().min(1).max(500),value:s.string().max(2e5),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ae=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ne=s.object({timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),De=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ie=s.object({key:s.string().min(1).max(500),value:s.unknown(),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),je=s.object({key:s.string().min(1).max(500),value:s.record(s.string(),s.unknown()),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Le=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),ze=s.object({key:s.string().min(1).max(500),state:s.record(s.string(),s.unknown()),version:s.number().int().optional(),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),We=s.object({key:s.string().min(1).max(500),state:s.record(s.string(),s.unknown()),version:s.number().int().optional(),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)});function d(e){return JSON.stringify(e)}function $(e){return JSON.stringify(e,null,2)}async function b(e,t){let n=await c.evaluate(e,t);if(n.error)throw new Error(n.error.message??"CDP evaluation failed.");let r=n.result&&typeof N(n.result)=="object"?N(n.result):void 0,o=r?.exceptionDetails&&typeof N(r.exceptionDetails)=="object"?r.exceptionDetails:void 0;if(o){let a=typeof o.text=="string"?o.text:"Runtime evaluation failed.",u=o.exception&&typeof N(o.exception)=="object"?o.exception:void 0,l=typeof u?.description=="string"?u.description:typeof u?.value=="string"?u.value:void 0;throw new Error(l?`${a}
29
+ ${l}`:a)}return(r?.result&&typeof N(r.result)=="object"?r.result:void 0)?.value}function N(e){return e&&typeof e=="object"?e:null}function _(e){return`(() => {
30
+ const mmkv = ${jt};
31
+ if (!mmkv) {
32
+ throw new Error("MMKV debug hook not found at globalThis.__EXPO_METRO_MCP__.mmkv");
33
+ }
34
+ if (typeof mmkv.getItem !== "function" || typeof mmkv.setItem !== "function" || typeof mmkv.removeItem !== "function" || typeof mmkv.getAllKeys !== "function") {
35
+ throw new Error("MMKV debug hook is present but missing one or more required methods: getItem, setItem, removeItem, getAllKeys");
36
+ }
37
+ return (${e})();
38
+ })()`}function L(e){return e==null?null:JSON.parse(e)}async function Je(e){y(e.key);let t=await b(_(`() => ({ key: ${d(e.key)}, value: mmkv.getItem(${d(e.key)}) ?? null })`),e.timeout_ms);return $(t)}async function Ke(e){M("mmkv_set"),y(e.key);let t=await b(_(`() => {
39
+ mmkv.setItem(${d(e.key)}, ${d(e.value)});
40
+ return { ok: true, key: ${d(e.key)}, value: mmkv.getItem(${d(e.key)}) ?? null };
41
+ }`),e.timeout_ms);return $(t)}async function Fe(e){M("mmkv_remove"),y(e.key);let t=await b(_(`() => {
42
+ mmkv.removeItem(${d(e.key)});
43
+ return { ok: true, key: ${d(e.key)} };
44
+ }`),e.timeout_ms);return $(t)}async function Be(e){let t=await b(_("() => ({ keys: mmkv.getAllKeys() })"),e.timeout_ms),n=t&&typeof t=="object"&&!Array.isArray(t)?t:{};return $({keys:Re(Array.isArray(n.keys)?n.keys:[])})}async function Ge(e){y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=L(t);return $({key:e.key,value:n})}async function D(e){M("mmkv_set_json"),y(e.key);let t=JSON.stringify(e.value),n=await b(_(`() => {
45
+ mmkv.setItem(${d(e.key)}, ${d(t)});
46
+ return { ok: true, key: ${d(e.key)}, value: JSON.parse(mmkv.getItem(${d(e.key)}) ?? "null") };
47
+ }`),e.timeout_ms);return $(n)}async function Ue(e){M("mmkv_merge_json"),y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=t==null?{}:L(t);if(!n||typeof n!="object"||Array.isArray(n))throw new Error("Existing MMKV value is not a JSON object, so merge would be sketchy. Use mmkv_set_json instead.");let r={...n,...e.value};return D({key:e.key,value:r,timeout_ms:e.timeout_ms})}async function Ve(e){y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=L(t);return $({key:e.key,state:n?.state??null,version:typeof n?.version=="number"?n.version:null,raw:n})}async function Xe(e){M("zustand_persist_set"),y(e.key);let t={state:e.state,...e.version!==void 0?{version:e.version}:{}};return D({key:e.key,value:t,timeout_ms:e.timeout_ms})}async function Ze(e){M("zustand_persist_merge"),y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=t==null?null:L(t),r=n?.state;if(r!=null&&(typeof r!="object"||Array.isArray(r)))throw new Error("Existing persisted Zustand state is not an object, so merge would be sketchy. Use zustand_persist_set instead.");let o={state:{...r??{},...e.state},version:e.version??(typeof n?.version=="number"?n.version:void 0)};return D({key:e.key,value:o,timeout_ms:e.timeout_ms})}var m=new Lt({name:"expo-metro-mcp",version:"1.0.7"});m.registerTool("get_logs",{description:"Fetch recent logs from the Metro dev server buffer. Supports filtering by level and time.",inputSchema:U.shape},async e=>({content:[{type:"text",text:V(e)}]}));m.registerTool("get_errors",{description:"Fetch recent errors from the Metro dev server buffer, with stack traces.",inputSchema:Z.shape},async e=>({content:[{type:"text",text:H(e)}]}));m.registerTool("get_status",{description:"Check the connection status of the Metro dev server and buffer statistics."},async()=>({content:[{type:"text",text:q()}]}));m.registerTool("connect",{description:"Grab the CDP connection to the Metro dev server. Use this if get_status shows disconnected, or to take over the connection from React Native DevTools."},async()=>({content:[{type:"text",text:await c.grabConnection()}]}));m.registerTool("disconnect",{description:"Release the CDP connection so React Native DevTools can connect. Use this before switching to DevTools. Call connect when you want to reattach."},async()=>(c.disconnect(),{content:[{type:"text",text:"Disconnected. DevTools can now connect freely."}]}));m.registerTool("clear_logs",{description:"Clear the internal log buffer. Useful after resolving an issue."},async()=>({content:[{type:"text",text:Y()}]}));m.registerTool("watch_logs",{description:"Listen for incoming logs for a short time window and return all collected entries.",inputSchema:Q.shape},async e=>({content:[{type:"text",text:await ee(e)}]}));m.registerTool("reload",{description:"Reload the React Native app via Metro."},async()=>(se(),{content:[{type:"text",text:await te()}]}));m.registerTool("resolve_stack",{description:"Resolve a stack trace from the buffer against the Metro source map, showing original file:line instead of bundle offsets. Optionally filter by error message substring.",inputSchema:ie.shape},async e=>({content:[{type:"text",text:await ae(e)}]}));m.registerTool("list_devices",{description:"List active iOS simulators and Android emulators. Use this to find available devices before taking screenshots or sending taps."},async()=>({content:[{type:"text",text:Me()}]}));m.registerTool("screenshot",{description:"Take a screenshot of the active iOS simulator or Android emulator. Returns the image directly. Optionally specify platform ('ios' or 'android') or device_id if multiple devices are running.",inputSchema:de.shape},async e=>{let t=fe(e);if(t.type==="image"){let n=t.width&&t.height?`Screenshot dimensions: ${t.width}x${t.height}px. Use these exact coordinates for tap and swipe \u2014 no scaling needed.`:"Screenshot dimensions unknown.";return{content:[{type:"image",data:t.data,mimeType:t.mimeType},{type:"text",text:n}]}}return{content:[{type:"text",text:t.text}]}});m.registerTool("tap",{description:"Tap at x,y coordinates on the active simulator/emulator. Use screenshot first to determine coordinates. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Optionally specify platform or device_id.",inputSchema:ge.shape},async e=>({content:[{type:"text",text:ye(e)}]}));m.registerTool("swipe",{description:"Swipe from one coordinate to another. Requires idb on iOS (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Useful for scrolling lists or dismissing sheets.",inputSchema:he.shape},async e=>({content:[{type:"text",text:be(e)}]}));m.registerTool("input_text",{description:"Type text into the currently focused input field on the active simulator/emulator. Use tap to focus an input first, then call this tool. Works without requiring the on-screen keyboard to be visible. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box.",inputSchema:_e.shape},async e=>({content:[{type:"text",text:xe(e)}]}));m.registerTool("input_key",{description:"Send a special key press to the active simulator/emulator. Supported keys: enter, backspace, delete, tab, escape, home, end, back, space, up, down, left, right. Use after input_text to submit forms (enter) or correct mistakes (backspace).",inputSchema:ke.shape},async e=>({content:[{type:"text",text:Se(e)}]}));A.enableEval&&m.registerTool("evaluate",{description:"Run JavaScript inside the connected React Native app runtime via Metro CDP. Supports async expressions, so you can inspect globals, read or mutate state, trigger navigation, or call app helpers directly.",inputSchema:Te.shape},async e=>({content:[{type:"text",text:await Ee(e)}]}));m.registerTool("mmkv_get",{description:"Read a value from the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv. Returns JSON with key and value.",inputSchema:Oe.shape},async e=>({content:[{type:"text",text:await Je(e)}]}));m.registerTool("mmkv_set",{description:"Write a string value through the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv. Useful for seeding persisted Zustand state before a screen renders.",inputSchema:Ce.shape},async e=>({content:[{type:"text",text:await Ke(e)}]}));m.registerTool("mmkv_remove",{description:"Remove a key through the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv.",inputSchema:Ae.shape},async e=>({content:[{type:"text",text:await Fe(e)}]}));m.registerTool("mmkv_keys",{description:"List all keys exposed by the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv.",inputSchema:Ne.shape},async e=>({content:[{type:"text",text:await Be(e)}]}));m.registerTool("mmkv_get_json",{description:"Read a JSON value from the app's MMKV debug hook and parse it before returning it.",inputSchema:De.shape},async e=>({content:[{type:"text",text:await Ge(e)}]}));m.registerTool("mmkv_set_json",{description:"Store any JSON-serializable value in MMKV through the app's debug hook. Safer than manually stringifying payloads in AI prompts.",inputSchema:Ie.shape},async e=>({content:[{type:"text",text:await D(e)}]}));m.registerTool("mmkv_merge_json",{description:"Merge a shallow JSON object into an existing MMKV JSON object. Good for patching persisted debug/config state without replacing the whole blob.",inputSchema:je.shape},async e=>({content:[{type:"text",text:await Ue(e)}]}));m.registerTool("zustand_persist_get",{description:"Read a persisted Zustand entry from MMKV and return its parsed state and version fields separately.",inputSchema:Le.shape},async e=>({content:[{type:"text",text:await Ve(e)}]}));m.registerTool("zustand_persist_set",{description:"Write a persisted Zustand payload to MMKV in the canonical { state, version? } shape.",inputSchema:ze.shape},async e=>({content:[{type:"text",text:await Xe(e)}]}));m.registerTool("zustand_persist_merge",{description:"Merge fields into the state object of an existing persisted Zustand MMKV entry, preserving version unless you override it.",inputSchema:We.shape},async e=>({content:[{type:"text",text:await Ze(e)}]}));async function Wt(){c.start();let e=new zt;await m.connect(e),process.on("SIGINT",()=>{c.stop(),process.exit(0)}),process.on("SIGTERM",()=>{c.stop(),process.exit(0)})}Wt().catch(e=>{process.stderr.write(`Fatal error: ${e}
24
48
  `),process.exit(1)});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@synnode/expo-metro-mcp",
3
- "version": "1.0.3",
4
- "description": "MCP server for Expo/React Native development — live logs, stack trace resolution, and simulator/emulator automation via CDP and platform CLIs.",
3
+ "version": "1.0.7",
4
+ "description": "MCP server for Expo/React Native development — live logs, stack trace resolution, runtime evaluation, and simulator/emulator automation via CDP and platform CLIs.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "expo-metro-mcp": "./dist/index.js"