@synnode/expo-metro-mcp 1.0.7 → 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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +33 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,7 +71,7 @@ claude mcp add expo-metro --env METRO_PORT=8082 npx @synnode/expo-metro-mcp
71
71
  | `resolve_stack` | Resolve a stack trace against the Metro source map, showing original file/line instead of bundle offsets |
72
72
  | `list_devices` | List active iOS simulators and Android emulators |
73
73
  | `screenshot` | Take a screenshot of the active simulator/emulator. Returns the image + pixel dimensions. Optional: `platform`, `device_id` |
74
- | `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) |
75
75
  | `swipe` | Swipe from one coordinate to another. Optional: `duration_ms`, `platform`, `device_id` |
76
76
  | `input_text` | Type text into the focused input field — works without the on-screen keyboard. Optional: `platform`, `device_id` |
77
77
  | `input_key` | Send a special key press: `enter`, `backspace`, `delete`, `tab`, `escape`, `back`, `space`, arrow keys. Optional: `platform`, `device_id` |
@@ -165,6 +165,7 @@ For most AI-driven state seeding, the Zustand helpers are the sweet spot. They a
165
165
  - To fill a form: `tap` the field → `input_text` the value → `input_key "enter"` to submit
166
166
  - If multiple devices are running, use `list_devices` to find the ID and pass it via `device_id`
167
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.
168
169
 
169
170
  ## Using alongside React Native DevTools
170
171
 
package/dist/index.js CHANGED
@@ -1,33 +1,37 @@
1
1
  #!/usr/bin/env node
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]
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
4
  ${E(n.message)}`).join(`
5
5
 
6
6
  ---
7
7
 
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(`
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=gt(n.rawMessage);if(!o.length)return`${r}
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 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).
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 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}):
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 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(`
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
- ${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
+ ${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 N(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 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}
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.7",
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": {