@synnode/expo-metro-mcp 1.0.6 → 1.0.8
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 +31 -4
- package/dist/index.js +34 -30
- package/package.json +1 -1
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:
|
|
@@ -55,11 +71,11 @@ claude mcp add expo-metro --env METRO_PORT=8082 npx @synnode/expo-metro-mcp
|
|
|
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
73
|
| `screenshot` | Take a screenshot of the active simulator/emulator. Returns the image + pixel dimensions. Optional: `platform`, `device_id` |
|
|
58
|
-
| `tap` | Tap at x,y coordinates on the active simulator/emulator. Optional: `platform`, `device_id` |
|
|
74
|
+
| `tap` | Tap at x,y coordinates on the active simulator/emulator. Optional: `platform`, `device_id`, `expected_package` (Android only — surfaces an error when an ANR dialog is focused and a warning when focus is on a different window or coords fall outside the focused frame) |
|
|
59
75
|
| `swipe` | Swipe from one coordinate to another. Optional: `duration_ms`, `platform`, `device_id` |
|
|
60
76
|
| `input_text` | Type text into the focused input field — works without the on-screen keyboard. Optional: `platform`, `device_id` |
|
|
61
77
|
| `input_key` | Send a special key press: `enter`, `backspace`, `delete`, `tab`, `escape`, `back`, `space`, arrow keys. Optional: `platform`, `device_id` |
|
|
62
|
-
| `evaluate` | Run JavaScript inside the connected app runtime via Metro CDP. Supports async expressions. Useful for reading state, calling app helpers, poking navigation, or mutating debug state. Optional: `timeout_ms` |
|
|
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` |
|
|
63
79
|
| `mmkv_get` | Read a raw string value from a dev-only MMKV debug hook exposed at `globalThis.__EXPO_METRO_MCP__.mmkv` |
|
|
64
80
|
| `mmkv_set` | Write a raw string value through that MMKV debug hook |
|
|
65
81
|
| `mmkv_remove` | Remove a key through that MMKV debug hook |
|
|
@@ -86,6 +102,8 @@ claude mcp add expo-metro --env METRO_PORT=8082 npx @synnode/expo-metro-mcp
|
|
|
86
102
|
- Expressions are awaited automatically, so `Promise` results work out of the box
|
|
87
103
|
- Returned values are serialized when possible; non-serializable objects fall back to their runtime description
|
|
88
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
|
|
89
107
|
|
|
90
108
|
## MMKV debug hook
|
|
91
109
|
|
|
@@ -97,14 +115,14 @@ Example app-side hook:
|
|
|
97
115
|
import {createMMKV} from "react-native-mmkv";
|
|
98
116
|
|
|
99
117
|
export const mmkv = createMMKV({
|
|
100
|
-
id: "
|
|
118
|
+
id: "demo-app",
|
|
101
119
|
});
|
|
102
120
|
|
|
103
121
|
if (__DEV__) {
|
|
104
122
|
globalThis.__EXPO_METRO_MCP__ = {
|
|
105
123
|
...globalThis.__EXPO_METRO_MCP__,
|
|
106
124
|
mmkv: {
|
|
107
|
-
id: "
|
|
125
|
+
id: "demo-app",
|
|
108
126
|
getItem: (key: string) => mmkv.getString(key) ?? null,
|
|
109
127
|
setItem: (key: string, value: string) => mmkv.set(key, value),
|
|
110
128
|
removeItem: (key: string) => mmkv.remove(key),
|
|
@@ -119,6 +137,11 @@ Once exposed, the MCP can use:
|
|
|
119
137
|
- JSON helpers: `mmkv_get_json`, `mmkv_set_json`, `mmkv_merge_json`
|
|
120
138
|
- Zustand helpers: `zustand_persist_get`, `zustand_persist_set`, `zustand_persist_merge`
|
|
121
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
|
+
|
|
122
145
|
This stays intentionally generic, so it works for persisted Zustand state and plain MMKV usage without coupling the MCP to your store internals.
|
|
123
146
|
|
|
124
147
|
For most AI-driven state seeding, the Zustand helpers are the sweet spot. They avoid hand-building the persisted wrapper shape every time.
|
|
@@ -142,13 +165,17 @@ For most AI-driven state seeding, the Zustand helpers are the sweet spot. They a
|
|
|
142
165
|
- To fill a form: `tap` the field → `input_text` the value → `input_key "enter"` to submit
|
|
143
166
|
- If multiple devices are running, use `list_devices` to find the ID and pass it via `device_id`
|
|
144
167
|
- iOS screenshots work without idb — only tap/swipe/input require it
|
|
168
|
+
- On Android, `tap` runs a focus pre-flight using `dumpsys window`. If a system ANR dialog has focus, the tap is blocked with recovery instructions (`adb shell input tap` would otherwise silently route the event to the dialog instead of your app). Pass `expected_package` to also catch stale Activity instances or other apps grabbing focus.
|
|
145
169
|
|
|
146
170
|
## Using alongside React Native DevTools
|
|
147
171
|
|
|
148
172
|
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.
|
|
149
173
|
|
|
174
|
+
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.
|
|
175
|
+
|
|
150
176
|
- **To use DevTools**: just open or reconnect it. The MCP will be disconnected automatically.
|
|
151
177
|
- **To return to MCP**: call `connect`. DevTools will lose its connection.
|
|
178
|
+
- **If a runtime tool fails due to no CDP connection**: retry after `connect`.
|
|
152
179
|
|
|
153
180
|
`disconnect` is available if you want to explicitly release the connection first, but it's not required.
|
|
154
181
|
|
package/dist/index.js
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as
|
|
3
|
-
`)}import{z as
|
|
4
|
-
${
|
|
2
|
+
import{McpServer as Gt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Ut}from"@modelcontextprotocol/sdk/server/stdio.js";import T from"ws";import et from"http";var tt=parseInt(process.env.METRO_PORT??"8081",10),nt=process.env.METRO_HOST??"localhost",rt=parseInt(process.env.LOG_BUFFER_SIZE??"1000",10),ot=3e4,V=1e3,it={log:"log",info:"info",warning:"warn",warn:"warn",error:"error",debug:"debug",dir:"log",dirxml:"log",table:"log",assert:"error"};function st(e){return e.map(t=>t.value!==void 0&&t.value!==null?String(t.value):t.description?t.description:"").filter(Boolean).join(" ")}async function at(e,t){return new Promise(n=>{let r=et.get(`http://${e}:${t}/json/list`,o=>{let i="";o.on("data",s=>i+=s),o.on("end",()=>{try{let s=JSON.parse(i);Array.isArray(s)?n(s):n([])}catch{n([])}})});r.on("error",()=>n([])),r.setTimeout(2e3,()=>{r.destroy(),n([])})})}var F=class{buffer=[];cdpWs=null;eventsWs=null;_connected=!1;_currentTargetId=null;_lastConnectedAt=null;_totalReceived=0;_stopped=!1;_eventsBackoff=V;_deviceTitle=null;_nextMessageId=1;pendingResponses=new Map;host=nt;port=tt;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>rt&&this.buffer.shift()}async checkForNewTarget(){let t=await at(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((s,c)=>{let u=setTimeout(()=>{this.pendingResponses.delete(i),c(new Error(`CDP command timed out after ${r}ms.`))},r);this.pendingResponses.set(i,{resolve:s,reject:c,timer:u}),o.send(JSON.stringify({id:i,method:t,params:n}),d=>{d&&(clearTimeout(u),this.pendingResponses.delete(i),c(d instanceof Error?d:new Error(String(d))))})})}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=it[n]??"log",o=Array.isArray(t.args)?t.args:[],i=st(o),s=typeof t.timestamp=="number"?Math.round(t.timestamp):Date.now();if(!i)return;let u=t.stackTrace?.callFrames?.filter(f=>f.url&&!f.url.startsWith("native")).map(f=>({functionName:f.functionName??"(anonymous)",url:f.url,line:f.lineNumber??0,col:f.columnNumber??0})),d=i.includes("http://")?i:void 0;this.addEntry({timestamp:s,level:r,message:i,rawMessage:d,rawFrames:u?.length?u: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=V}),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,ot),this.connectEvents()},this._eventsBackoff)}},l=new F;import{z as j}from"zod";var ct=/\(https?:\/\/[^)]+\.bundle[^)]*:(\d+:\d+)\)/g;function E(e){return e.replace(ct,"(:$1)")}function R(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 H=j.object({lines:j.coerce.number().int().min(1).max(500).optional().default(50),level:j.enum(["error","warn","info","log","debug"]).optional(),since:j.string().optional()});function ut(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 X(e){let t=e.since?ut(e.since):void 0,n=l.getEntries({lines:e.lines,level:e.level,since:t});return n.length===0?"No log entries found.":n.map(r=>`[${R(r.timestamp)}] [${r.level.toUpperCase()}] ${E(r.message)}`).join(`
|
|
3
|
+
`)}import{z as Z}from"zod";var q=Z.object({lines:Z.coerce.number().int().min(1).max(200).optional().default(20)});function Y(e){let t=l.getEntries({level:"error",lines:e.lines});return t.length===0?"No errors in buffer.":t.map(n=>`[${R(n.timestamp)}] [ERROR]
|
|
4
|
+
${E(n.message)}`).join(`
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
`)}function
|
|
9
|
-
`)}import
|
|
8
|
+
`)}function Q(){let e={connected:l.connected,host:l.host,port:l.port,device:l.deviceTitle,buffered_entries:l.bufferedEntries,last_connected_at:l.lastConnectedAt?.toISOString()??null,total_received:l.totalReceived,expo_sdk_version:null};return JSON.stringify(e,null,2)}function ee(){let e=l.clearBuffer();return`Cleared ${e} log ${e===1?"entry":"entries"} from the buffer.`}import{z}from"zod";var te=z.object({duration:z.string().optional().default("10s"),level:z.enum(["error","warn","info","log","debug"]).optional()});function lt(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 mt=500;async function ne(e){if(!l.connected)return"Metro is not connected. Start Expo dev server and try again.";let t=lt(e.duration),n=l.bufferedEntries,r=Date.now(),o=e.level;await new Promise(u=>{let d=setInterval(()=>{Date.now()-r>=t&&(clearInterval(d),u())},mt)});let s=l.getEntries({lines:l.bufferedEntries}).slice(n),c=o?s.filter(u=>u.level===o):s;return c.length===0?`No logs received during ${e.duration} window.`:c.map(u=>`[${R(u.timestamp)}] [${u.level.toUpperCase()}] ${E(u.message)}`).join(`
|
|
9
|
+
`)}import dt from"http";var pt=parseInt(process.env.METRO_PORT??"8081",10),ft=process.env.METRO_HOST??"localhost";async function re(){return new Promise(e=>{let t=dt.request({hostname:ft,port:pt,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 oe}from"zod";import gt from"http";import{SourceMapConsumer as ht}from"source-map";var ae=oe.object({message:oe.string().optional()}),L=new Map,yt=6e4;function ie(e,t,n){return e.replace(/^https?:\/\/[^/]+/,`http://${t}:${n}`)}function se(e){let t=e.match(/(?:\/\/&|\?)(.+)$/),n=t?t[1]:"dev=true&minify=false";return`${e.replace(/(\/[^/?]+)\.bundle.*$/,"$1.map")}?${n}`}async function vt(e){let t=L.get(e);return t&&Date.now()-t.fetchedAt<yt?t.consumer:new Promise(n=>{let r=new URL(e),o=gt.get({hostname:r.hostname,port:Number(r.port)||8081,path:r.pathname+r.search},i=>{let s=[];i.on("data",c=>s.push(c)),i.on("end",async()=>{if(i.statusCode!==200){n(null);return}try{let c=JSON.parse(Buffer.concat(s).toString()),u=await ht.with(c,null,d=>d);L.set(e,{consumer:u,fetchedAt:Date.now()}),n(u)}catch{n(null)}})});o.on("error",()=>n(null)),o.setTimeout(1e4,()=>{o.destroy(),n(null)})})}function ce(){L.forEach(e=>e.consumer.destroy()),L.clear()}function bt(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 wt(e,t){let n=[],r=new Map;for(let i of e){let s=ie(i.url,l.host,l.port),c=se(s);r.has(c)||r.set(c,[]),r.get(c).push(i)}let o=new Map;await Promise.all([...r.keys()].map(async i=>{o.set(i,await vt(i))}));for(let i of e){let s=ie(i.url,l.host,l.port),c=se(s),u=o.get(c)??null;if(u){let d=u.originalPositionFor({line:i.line+1,column:i.col});if(d.source){let f=d.source.replace(/^.*\/\/\//,"").replace(/\?.*$/,"");n.push(` at ${i.functionName} (${f}:${d.line}:${d.column})`);continue}}n.push(` at ${i.functionName} (:${i.line+1}:${i.col})`)}return n}async function ue(e){let t=l.getEntries({level:"error",lines:50}),n=e.message?[...t].reverse().find(u=>u.message.includes(e.message)):t.at(-1);if(!n)return"No error entries in buffer.";let r=`[ERROR] ${n.message.split(`
|
|
10
10
|
`)[0]}`;if(!n.rawMessage)return`${r}
|
|
11
11
|
|
|
12
|
-
No stack frames available.`;let o=
|
|
12
|
+
No stack frames available.`;let o=bt(n.rawMessage);if(!o.length)return`${r}
|
|
13
13
|
|
|
14
|
-
No stack frames available.`;let i=await
|
|
15
|
-
`)}import{z as
|
|
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],
|
|
14
|
+
No stack frames available.`;let i=await wt(o),s=i.filter(u=>!u.includes("node_modules")&&!u.includes("(:")),c=[r,""];return s.length?c.push(...s):c.push(...i),c.join(`
|
|
15
|
+
`)}import{z as J}from"zod";import{execSync as x}from"child_process";import*as S from"fs";import*as pe from"os";import*as fe from"path";import{execSync as le}from"child_process";function me(e){try{return le(e,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return""}}function de(e){try{return le(`which ${e}`,{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function kt(){if(!de("xcrun"))return[];let e=me("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 _t(){if(!de("adb"))return[];let e=me("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],s=r.match(/model:(\S+)/),c=s?s[1].replace(/_/g," "):i;t.push({id:i,name:c,platform:"android"})}return t}function h(){return[...kt(),..._t()]}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 ge=J.object({device_id:J.string().optional(),platform:J.enum(["ios","android"]).optional()});function xt(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]),s=Math.max(o,i);return s>=2500&&o%3===0||s>=2500?3:s>=1334?2:1}catch{return 1}}function St(e,t){x(`xcrun simctl io "${e}" screenshot "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});let n=xt(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 s=Math.round(parseInt(o[1])/n),c=Math.round(parseInt(i[1])/n),u=t.replace(".png","-points.png");x(`sips -z ${c} ${s} "${t}" --out "${u}"`,{timeout:1e4,stdio:"ignore"}),S.renameSync(u,t)}}catch{}}function $t(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 Mt(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 he(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=fe.join(pe.tmpdir(),`expo-mcp-screenshot-${Date.now()}.png`);try{n.platform==="ios"?St(n.id,r):$t(n.id,r);let o=S.readFileSync(r),i=o.toString("base64"),s=Mt(o);return S.unlinkSync(r),{type:"image",data:i,mimeType:"image/png",width:s?.width??0,height:s?.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 g}from"zod";import{execSync as O}from"child_process";import{spawn as Tt}from"child_process";var y=null,K=null;function P(e){y&&!y.killed&&K===e||(y&&!y.killed&&y.kill(),y=Tt("idb_companion",["--udid",e],{detached:!1,stdio:"ignore"}),K=e,y.on("exit",()=>{y=null,K=null}),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,800))}process.on("exit",()=>y?.kill());process.on("SIGTERM",()=>{y?.kill(),process.exit(0)});process.on("SIGINT",()=>{y?.kill(),process.exit(0)});import{execSync as Et}from"child_process";function Rt(e){try{return Et(`adb -s "${e}" shell dumpsys window`,{timeout:5e3,stdio:["ignore","pipe","ignore"],maxBuffer:8*1024*1024}).toString()}catch{return""}}function Pt(e){let t=e.match(/mFrame=Rect\((-?\d+),\s*(-?\d+)\s*-\s*(-?\d+),\s*(-?\d+)\)/);if(t)return{x1:Number(t[1]),y1:Number(t[2]),x2:Number(t[3]),y2:Number(t[4])};let n=e.match(/\bframe=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/);return n?{x1:Number(n[1]),y1:Number(n[2]),x2:Number(n[3]),y2:Number(n[4])}:null}function ye(e){let t=Rt(e);if(!t)return null;let n=t.match(/mCurrentFocus=Window\{([0-9a-f]+)\s+u\d+\s+([^}]+)\}/);if(!n)return null;let r=n[1],o=n[2].trim(),i=o.indexOf("/"),s=i>0&&!o.includes(" "),c=!s,u=/^Application Not Responding\b/i.test(o),d=s?o.slice(0,i):null,f=s?o.slice(i+1):null,A=null,G=new RegExp(`Window\\{${r}\\s+u\\d+\\s+[^}]+\\}:`).exec(t);if(G){let U=G.index,Qe=t.slice(U,U+4e3);A=Pt(Qe)}return{windowHash:r,displayName:o,package:d,activity:f,isSystemDialog:c,isAnrDialog:u,frame:A}}var ve=g.object({x:g.number().int().describe("X coordinate in points/pixels"),y:g.number().int().describe("Y coordinate in points/pixels"),device_id:g.string().optional(),platform:g.enum(["ios","android"]).optional(),expected_package:g.string().optional().describe("Android only \u2014 package name that should currently have focus (e.g. 'net.synnode.nullshift'). If set, a warning is returned when focus is on a different window. Use this to detect ANR dialogs, permission prompts, or stale Activity instances silently swallowing taps.")}),be=g.object({x1:g.number().int(),y1:g.number().int(),x2:g.number().int(),y2:g.number().int(),duration_ms:g.number().int().min(50).max(5e3).optional().default(300),device_id:g.string().optional(),platform:g.enum(["ios","android"]).optional()});function we(){try{return O("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function Ot(e,t,n){if(we()){P(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
|
|
19
|
-
Install via: brew install idb-companion && pip3 install fb-idb`)}function
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Or: brew install facebook/fb/idb-companion`)}function At(e,t,n,r){let o=[],i=ye(e);if(i?.isAnrDialog)throw new Error(`Tap blocked: an ANR ('Application Not Responding') system dialog has focus on this device ('${i.displayName}'). Synthetic taps via 'adb input tap' route to the focused window, so this tap would never reach your app. Resolve via: 'adb shell am force-stop ${r??"<your-package>"}' followed by 'adb reboot', or 'adb shell pm clear ${r??"<your-package>"}' (destroys app data).`);if(i&&r&&i.package!==r&&o.push(`Focused window is '${i.displayName}', not '${r}'. The tap may not reach your app \u2014 common causes: system dialog on top, stale Activity instance focused, or a different app in the foreground.`),i?.frame){let{x1:s,y1:c,x2:u,y2:d}=i.frame;u>s&&d>c&&(t<s||t>u||n<c||n>d)&&o.push(`Tap coordinates (${t}, ${n}) fall outside the focused window's frame [${s},${c}]-[${u},${d}]. The event may not be dispatched to '${i.displayName}'.`)}return O(`adb -s "${e}" shell input tap ${t} ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]}),{warnings:o}}function Nt(e,t,n,r,o,i){if(we()){P(e);let s=(i/1e3).toFixed(2);O(`idb ui swipe ${t} ${n} ${r} ${o} ${s} --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 Ct(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 ke(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{if(n.platform==="ios")return Ot(n.id,e.x,e.y),`Tapped (${e.x}, ${e.y}) on ${n.name} [ios].`;let{warnings:r}=At(n.id,e.x,e.y,e.expected_package),o=`Tapped (${e.x}, ${e.y}) on ${n.name} [android].`;return r.length&&(o+=`
|
|
20
|
+
|
|
21
|
+
Warnings:
|
|
22
|
+
- ${r.join(`
|
|
23
|
+
- `)}`),o}catch(r){return`Tap failed: ${r instanceof Error?r.message:String(r)}`}}function _e(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"?(Nt(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.`):(Ct(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 _}from"zod";import{execSync as N}from"child_process";var xe=_.object({text:_.string().describe("Text to type into the focused input field"),device_id:_.string().optional(),platform:_.enum(["ios","android"]).optional()}),Se=_.object({key:_.enum(["enter","backspace","delete","tab","escape","home","end","back","space","up","down","left","right"]).describe("Key to press"),device_id:_.string().optional(),platform:_.enum(["ios","android"]).optional()}),Dt={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},It={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 $e(){try{return N("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function jt(e,t){if($e()){P(e);let n=t.replace(/'/g,`'"'"'`);N(`idb ui text '${n}' --udid "${e}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS text input requires idb.
|
|
24
|
+
Install via: brew install idb-companion && pip3 install fb-idb`)}function Lt(e,t){let n=t.replace(/\\/g,"\\\\").replace(/ /g,"%s").replace(/'/g,"\\'").replace(/"/g,'\\"');N(`adb -s "${e}" shell input text "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function Wt(e,t){if($e()){P(e);let n=It[t];if(n===void 0)throw new Error(`Unknown key: ${t}`);N(`idb ui key ${n} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS key input requires idb.
|
|
25
|
+
Install via: brew install idb-companion && pip3 install fb-idb`)}function Ft(e,t){let n=Dt[t];if(n===void 0)throw new Error(`Unknown key: ${t}`);N(`adb -s "${e}" shell input keyevent ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function Me(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"?jt(n.id,e.text):Lt(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 Te(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"?Wt(n.id,e.key):Ft(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 Ee(){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
26
|
${t.join(`
|
|
23
|
-
`)}`}import{z as
|
|
24
|
-
${r}`:t}async function
|
|
25
|
-
${
|
|
27
|
+
`)}`}import{z as B}from"zod";var Pe=B.object({code:B.string().min(1).max(2e4),timeout_ms:B.coerce.number().int().min(100).max(3e4).optional().default(5e3)});function Re(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 zt(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}
|
|
28
|
+
${r}`:t}async function Oe(e){let t=await l.evaluate(e.code,e.timeout_ms);if(t.error){let A=[t.error.message??"CDP error"];return t.error.data!==void 0&&A.push(Re(t.error.data)),`Evaluation failed.
|
|
29
|
+
${A.join(`
|
|
26
30
|
`)}`}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
|
-
${
|
|
28
|
-
${
|
|
29
|
-
${
|
|
30
|
-
const mmkv = ${
|
|
31
|
+
${zt(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",s=typeof o.subtype=="string"?o.subtype:void 0,c=o.value,u=typeof o.description=="string"?o.description:void 0,d=[`type: ${i}`];s&&d.push(`subtype: ${s}`);let f=c!==void 0?Re(c):u??"(no serializable value)";return`${d.join(", ")}
|
|
32
|
+
${f}`}var Jt=new Set(["1","true","yes","on"]);function Ae(e,t=!1){let n=process.env[e];return n==null?t:Jt.has(n.trim().toLowerCase())}function Kt(e){let t=process.env[e];return t?t.split(",").map(n=>n.trim()).filter(Boolean):[]}var C={readOnly:Ae("EXPO_METRO_MCP_READ_ONLY"),enableEval:Ae("EXPO_METRO_MCP_ENABLE_EVAL"),mmkvPrefixAllowlist:Kt("EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST")};function $(e){if(C.readOnly)throw new Error(`${e} is disabled because EXPO_METRO_MCP_READ_ONLY=1.`)}function v(e){let{mmkvPrefixAllowlist:t}=C;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 Ne(e){let{mmkvPrefixAllowlist:t}=C;return t.length?e.filter(n=>t.some(r=>n.startsWith(r))):e}import{z as a}from"zod";var Bt="globalThis.__EXPO_METRO_MCP__?.mmkv",Ce=a.object({key:a.string().min(1).max(500),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),De=a.object({key:a.string().min(1).max(500),value:a.string().max(2e5),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ie=a.object({key:a.string().min(1).max(500),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),je=a.object({timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Le=a.object({key:a.string().min(1).max(500),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),We=a.object({key:a.string().min(1).max(500),value:a.unknown(),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Fe=a.object({key:a.string().min(1).max(500),value:a.record(a.string(),a.unknown()),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),ze=a.object({key:a.string().min(1).max(500),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Je=a.object({key:a.string().min(1).max(500),state:a.record(a.string(),a.unknown()),version:a.number().int().optional(),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ke=a.object({key:a.string().min(1).max(500),state:a.record(a.string(),a.unknown()),version:a.number().int().optional(),timeout_ms:a.coerce.number().int().min(100).max(3e4).optional().default(5e3)});function p(e){return JSON.stringify(e)}function M(e){return JSON.stringify(e,null,2)}async function b(e,t){let n=await l.evaluate(e,t);if(n.error)throw new Error(n.error.message??"CDP evaluation failed.");let r=n.result&&typeof D(n.result)=="object"?D(n.result):void 0,o=r?.exceptionDetails&&typeof D(r.exceptionDetails)=="object"?r.exceptionDetails:void 0;if(o){let s=typeof o.text=="string"?o.text:"Runtime evaluation failed.",c=o.exception&&typeof D(o.exception)=="object"?o.exception:void 0,u=typeof c?.description=="string"?c.description:typeof c?.value=="string"?c.value:void 0;throw new Error(u?`${s}
|
|
33
|
+
${u}`:s)}return(r?.result&&typeof D(r.result)=="object"?r.result:void 0)?.value}function D(e){return e&&typeof e=="object"?e:null}function w(e){return`(() => {
|
|
34
|
+
const mmkv = ${Bt};
|
|
31
35
|
if (!mmkv) {
|
|
32
36
|
throw new Error("MMKV debug hook not found at globalThis.__EXPO_METRO_MCP__.mmkv");
|
|
33
37
|
}
|
|
@@ -35,14 +39,14 @@ ${l}`:a)}return(r?.result&&typeof O(r.result)=="object"?r.result:void 0)?.value}
|
|
|
35
39
|
throw new Error("MMKV debug hook is present but missing one or more required methods: getItem, setItem, removeItem, getAllKeys");
|
|
36
40
|
}
|
|
37
41
|
return (${e})();
|
|
38
|
-
})()`}function
|
|
39
|
-
mmkv.setItem(${
|
|
40
|
-
return { ok: true, key: ${
|
|
41
|
-
}`),e.timeout_ms);return
|
|
42
|
-
mmkv.removeItem(${
|
|
43
|
-
return { ok: true, key: ${
|
|
44
|
-
}`),e.timeout_ms);return
|
|
45
|
-
mmkv.setItem(${
|
|
46
|
-
return { ok: true, key: ${
|
|
47
|
-
}`),e.timeout_ms);return
|
|
42
|
+
})()`}function W(e){return e==null?null:JSON.parse(e)}async function Be(e){v(e.key);let t=await b(w(`() => ({ key: ${p(e.key)}, value: mmkv.getItem(${p(e.key)}) ?? null })`),e.timeout_ms);return M(t)}async function Ge(e){$("mmkv_set"),v(e.key);let t=await b(w(`() => {
|
|
43
|
+
mmkv.setItem(${p(e.key)}, ${p(e.value)});
|
|
44
|
+
return { ok: true, key: ${p(e.key)}, value: mmkv.getItem(${p(e.key)}) ?? null };
|
|
45
|
+
}`),e.timeout_ms);return M(t)}async function Ue(e){$("mmkv_remove"),v(e.key);let t=await b(w(`() => {
|
|
46
|
+
mmkv.removeItem(${p(e.key)});
|
|
47
|
+
return { ok: true, key: ${p(e.key)} };
|
|
48
|
+
}`),e.timeout_ms);return M(t)}async function Ve(e){let t=await b(w("() => ({ keys: mmkv.getAllKeys() })"),e.timeout_ms),n=t&&typeof t=="object"&&!Array.isArray(t)?t:{};return M({keys:Ne(Array.isArray(n.keys)?n.keys:[])})}async function He(e){v(e.key);let t=await b(w(`() => mmkv.getItem(${p(e.key)}) ?? null`),e.timeout_ms),n=W(t);return M({key:e.key,value:n})}async function I(e){$("mmkv_set_json"),v(e.key);let t=JSON.stringify(e.value),n=await b(w(`() => {
|
|
49
|
+
mmkv.setItem(${p(e.key)}, ${p(t)});
|
|
50
|
+
return { ok: true, key: ${p(e.key)}, value: JSON.parse(mmkv.getItem(${p(e.key)}) ?? "null") };
|
|
51
|
+
}`),e.timeout_ms);return M(n)}async function Xe(e){$("mmkv_merge_json"),v(e.key);let t=await b(w(`() => mmkv.getItem(${p(e.key)}) ?? null`),e.timeout_ms),n=t==null?{}:W(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 I({key:e.key,value:r,timeout_ms:e.timeout_ms})}async function Ze(e){v(e.key);let t=await b(w(`() => mmkv.getItem(${p(e.key)}) ?? null`),e.timeout_ms),n=W(t);return M({key:e.key,state:n?.state??null,version:typeof n?.version=="number"?n.version:null,raw:n})}async function qe(e){$("zustand_persist_set"),v(e.key);let t={state:e.state,...e.version!==void 0?{version:e.version}:{}};return I({key:e.key,value:t,timeout_ms:e.timeout_ms})}async function Ye(e){$("zustand_persist_merge"),v(e.key);let t=await b(w(`() => mmkv.getItem(${p(e.key)}) ?? null`),e.timeout_ms),n=t==null?null:W(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 I({key:e.key,value:o,timeout_ms:e.timeout_ms})}var m=new Gt({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:H.shape},async e=>({content:[{type:"text",text:X(e)}]}));m.registerTool("get_errors",{description:"Fetch recent errors from the Metro dev server buffer, with stack traces.",inputSchema:q.shape},async e=>({content:[{type:"text",text:Y(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 l.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()=>(l.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:ee()}]}));m.registerTool("watch_logs",{description:"Listen for incoming logs for a short time window and return all collected entries.",inputSchema:te.shape},async e=>({content:[{type:"text",text:await ne(e)}]}));m.registerTool("reload",{description:"Reload the React Native app via Metro."},async()=>(ce(),{content:[{type:"text",text:await re()}]}));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:ae.shape},async e=>({content:[{type:"text",text:await ue(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:Ee()}]}));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:ge.shape},async e=>{let t=he(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:ve.shape},async e=>({content:[{type:"text",text:ke(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:be.shape},async e=>({content:[{type:"text",text:_e(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:xe.shape},async e=>({content:[{type:"text",text:Me(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:Se.shape},async e=>({content:[{type:"text",text:Te(e)}]}));C.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:Pe.shape},async e=>({content:[{type:"text",text:await Oe(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:Ce.shape},async e=>({content:[{type:"text",text:await Be(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:De.shape},async e=>({content:[{type:"text",text:await Ge(e)}]}));m.registerTool("mmkv_remove",{description:"Remove a key through the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv.",inputSchema:Ie.shape},async e=>({content:[{type:"text",text:await Ue(e)}]}));m.registerTool("mmkv_keys",{description:"List all keys exposed by the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv.",inputSchema:je.shape},async e=>({content:[{type:"text",text:await Ve(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:Le.shape},async e=>({content:[{type:"text",text:await He(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:We.shape},async e=>({content:[{type:"text",text:await I(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:Fe.shape},async e=>({content:[{type:"text",text:await Xe(e)}]}));m.registerTool("zustand_persist_get",{description:"Read a persisted Zustand entry from MMKV and return its parsed state and version fields separately.",inputSchema:ze.shape},async e=>({content:[{type:"text",text:await Ze(e)}]}));m.registerTool("zustand_persist_set",{description:"Write a persisted Zustand payload to MMKV in the canonical { state, version? } shape.",inputSchema:Je.shape},async e=>({content:[{type:"text",text:await qe(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:Ke.shape},async e=>({content:[{type:"text",text:await Ye(e)}]}));async function Vt(){l.start();let e=new Ut;await m.connect(e),process.on("SIGINT",()=>{l.stop(),process.exit(0)}),process.on("SIGTERM",()=>{l.stop(),process.exit(0)})}Vt().catch(e=>{process.stderr.write(`Fatal error: ${e}
|
|
48
52
|
`),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnode/expo-metro-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
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": {
|