@synnode/expo-metro-mcp 1.0.1 → 1.0.2
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 +8 -0
- package/dist/index.js +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,14 @@ CDP only allows one client at a time. Switching between the MCP and DevTools is
|
|
|
91
91
|
|
|
92
92
|
Metro exposes a CDP WebSocket at `/inspector/debug`. On `connect`, the server calls `/json/list` to discover the active device target, then attaches via CDP and enables `Runtime.consoleAPICalled` events. Metro build errors (`build_failed`, `bundling_error`) are captured separately via the `/events` WebSocket, which reconnects automatically.
|
|
93
93
|
|
|
94
|
+
## Teaching Claude Code about this MCP
|
|
95
|
+
|
|
96
|
+
Add [`SKILL.md`](./SKILL.md) to your project root (or `CLAUDE.md`) to teach Claude Code how to use this MCP effectively — when to check logs, how to debug errors, how to use screenshots and taps, and more.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -o SKILL.md https://raw.githubusercontent.com/Synnode/expo-metro-mcp/master/SKILL.md
|
|
100
|
+
```
|
|
101
|
+
|
|
94
102
|
## Notes
|
|
95
103
|
|
|
96
104
|
- If Metro is not reachable on startup: the server starts normally, `get_status` returns `connected: false`. Call `connect` once your dev server is up.
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as
|
|
3
|
-
`)}import{z as A}from"zod";var N=A.object({lines:A.coerce.number().int().min(1).max(200).optional().default(20)});function D(e){let
|
|
4
|
-
${b(
|
|
2
|
+
import{McpServer as Fe}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as We}from"@modelcontextprotocol/sdk/server/stdio.js";import $ from"ws";import se from"http";var ce=parseInt(process.env.METRO_PORT??"8081",10),ae=process.env.METRO_HOST??"localhost",le=parseInt(process.env.LOG_BUFFER_SIZE??"1000",10),ue=3e4,C=1e3,me={log:"log",info:"info",warning:"warn",warn:"warn",error:"error",debug:"debug",dir:"log",dirxml:"log",table:"log",assert:"error"};function pe(e){return e.map(t=>t.value!==void 0&&t.value!==null?String(t.value):t.description?t.description:"").filter(Boolean).join(" ")}async function de(e,t){return new Promise(n=>{let r=se.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 T=class{buffer=[];cdpWs=null;eventsWs=null;_connected=!1;_currentTargetId=null;_lastConnectedAt=null;_totalReceived=0;_stopped=!1;_eventsBackoff=C;_deviceTitle=null;host=ae;port=ce;get connected(){return this._connected}get lastConnectedAt(){return this._lastConnectedAt}get totalReceived(){return this._totalReceived}get bufferedEntries(){return this.buffer.length}get deviceTitle(){return this._deviceTitle}start(){this._stopped=!1,this.connectEvents()}stop(){this._stopped=!0,this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this.eventsWs&&(this.eventsWs.terminate(),this.eventsWs=null)}disconnect(){this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._connected=!1,this._currentTargetId=null,this._deviceTitle=null}getEntries(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?"}addEntry(t){this._totalReceived++,this.buffer.push(t),this.buffer.length>le&&this.buffer.shift()}async checkForNewTarget(){let t=await de(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===$.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.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}o.method==="Runtime.consoleAPICalled"&&o.params&&this.handleConsoleEvent(o.params)}),n.on("close",()=>{this.cdpWs===n&&(this._connected=!1,this.cdpWs=null,this._currentTargetId=null,this._deviceTitle=null)}),n.on("error",()=>{})}handleConsoleEvent(t){let n=typeof t.type=="string"?t.type:"log",r=me[n]??"log",o=Array.isArray(t.args)?t.args:[],i=pe(o),s=typeof t.timestamp=="number"?Math.round(t.timestamp):Date.now();if(!i)return;let a=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:a?.length?a:void 0})}connectEvents(){if(this._stopped)return;let t=`ws://${this.host}:${this.port}/events`,n;try{n=new $(t)}catch{this.scheduleEventsReconnect();return}this.eventsWs=n,n.on("open",()=>{this._eventsBackoff=C}),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,ue),this.connectEvents()},this._eventsBackoff)}},c=new T;import{z as x}from"zod";var fe=/\(https?:\/\/[^)]+\.bundle[^)]*:(\d+:\d+)\)/g;function b(e){return e.replace(fe,"(:$1)")}function y(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 O=x.object({lines:x.coerce.number().int().min(1).max(500).optional().default(50),level:x.enum(["error","warn","info","log","debug"]).optional(),since:x.string().optional()});function ge(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 L(e){let t=e.since?ge(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=>`[${y(r.timestamp)}] [${r.level.toUpperCase()}] ${b(r.message)}`).join(`
|
|
3
|
+
`)}import{z as A}from"zod";var N=A.object({lines:A.coerce.number().int().min(1).max(200).optional().default(20)});function D(e){let t=c.getEntries({level:"error",lines:e.lines});return t.length===0?"No errors in buffer.":t.map(n=>`[${y(n.timestamp)}] [ERROR]
|
|
4
|
+
${b(n.message)}`).join(`
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
`)}function
|
|
9
|
-
`)}import be from"http";var ye=parseInt(process.env.METRO_PORT??"8081",10),Se=process.env.METRO_HOST??"localhost";async function B(){return new Promise(e=>{let
|
|
10
|
-
`)[0]}`;if(!
|
|
8
|
+
`)}function I(){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 P(){let e=c.clearBuffer();return`Cleared ${e} log ${e===1?"entry":"entries"} from the buffer.`}import{z as k}from"zod";var F=k.object({duration:k.string().optional().default("10s"),level:k.enum(["error","warn","info","log","debug"]).optional()});function he(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 ve=500;async function W(e){if(!c.connected)return"Metro is not connected. Start Expo dev server and try again.";let t=he(e.duration),n=c.bufferedEntries,r=Date.now(),o=e.level;await new Promise(a=>{let d=setInterval(()=>{Date.now()-r>=t&&(clearInterval(d),a())},ve)});let s=c.getEntries({lines:c.bufferedEntries}).slice(n),l=o?s.filter(a=>a.level===o):s;return l.length===0?`No logs received during ${e.duration} window.`:l.map(a=>`[${y(a.timestamp)}] [${a.level.toUpperCase()}] ${b(a.message)}`).join(`
|
|
9
|
+
`)}import be from"http";var ye=parseInt(process.env.METRO_PORT??"8081",10),Se=process.env.METRO_HOST??"localhost";async function B(){return new Promise(e=>{let t=be.request({hostname:Se,port:ye,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 j}from"zod";import we from"http";import{SourceMapConsumer as xe}from"source-map";var G=j.object({message:j.string().optional()}),_=new Map,_e=6e4;function U(e,t,n){return e.replace(/^https?:\/\/[^/]+/,`http://${t}:${n}`)}function z(e){let t=e.match(/(?:\/\/&|\?)(.+)$/),n=t?t[1]:"dev=true&minify=false";return`${e.replace(/(\/[^/?]+)\.bundle.*$/,"$1.map")}?${n}`}async function $e(e){let t=_.get(e);return t&&Date.now()-t.fetchedAt<_e?t.consumer:new Promise(n=>{let r=new URL(e),o=we.get({hostname:r.hostname,port:Number(r.port)||8081,path:r.pathname+r.search},i=>{let s=[];i.on("data",l=>s.push(l)),i.on("end",async()=>{if(i.statusCode!==200){n(null);return}try{let l=JSON.parse(Buffer.concat(s).toString()),a=await xe.with(l,null,d=>d);_.set(e,{consumer:a,fetchedAt:Date.now()}),n(a)}catch{n(null)}})});o.on("error",()=>n(null)),o.setTimeout(1e4,()=>{o.destroy(),n(null)})})}function H(){_.forEach(e=>e.consumer.destroy()),_.clear()}function Te(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 ke(e,t){let n=[],r=new Map;for(let i of e){let s=U(i.url,c.host,c.port),l=z(s);r.has(l)||r.set(l,[]),r.get(l).push(i)}let o=new Map;await Promise.all([...r.keys()].map(async i=>{o.set(i,await $e(i))}));for(let i of e){let s=U(i.url,c.host,c.port),l=z(s),a=o.get(l)??null;if(a){let d=a.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 J(e){let t=c.getEntries({level:"error",lines:50}),n=e.message?[...t].reverse().find(a=>a.message.includes(e.message)):t.at(-1);if(!n)return"No error entries in buffer.";let r=`[ERROR] ${n.message.split(`
|
|
10
|
+
`)[0]}`;if(!n.rawMessage)return`${r}
|
|
11
11
|
|
|
12
|
-
No stack frames available.`;let o=Te(
|
|
12
|
+
No stack frames available.`;let o=Te(n.rawMessage);if(!o.length)return`${r}
|
|
13
13
|
|
|
14
|
-
No stack frames available.`;let i=await ke(o),
|
|
15
|
-
`)}import{z as
|
|
16
|
-
`).slice(1);for(let r of
|
|
14
|
+
No stack frames available.`;let i=await ke(o),s=i.filter(a=>!a.includes("node_modules")&&!a.includes("(:")),l=[r,""];return s.length?l.push(...s):l.push(...i),l.join(`
|
|
15
|
+
`)}import{z as E}from"zod";import{execSync as h}from"child_process";import*as v from"fs";import*as Y from"os";import*as Z from"path";import{execSync as q}from"child_process";function X(e){try{return q(e,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return""}}function K(e){try{return q(`which ${e}`,{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function Ee(){if(!K("xcrun"))return[];let e=X("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 Me(){if(!K("adb"))return[];let e=X("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+)/),l=s?s[1].replace(/_/g," "):i;t.push({id:i,name:l,platform:"android"})}return t}function g(){return[...Ee(),...Me()]}function w(e,t){let n=g();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 V=E.object({device_id:E.string().optional(),platform:E.enum(["ios","android"]).optional()});function Re(e){try{let t=h(`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 Ce(e,t){h(`xcrun simctl io "${e}" screenshot "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});let n=Re(t);if(n>1)try{let r=h(`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),l=Math.round(parseInt(i[1])/n),a=t.replace(".png","-points.png");h(`sips -z ${l} ${s} "${t}" --out "${a}"`,{timeout:1e4,stdio:"ignore"}),v.renameSync(a,t)}}catch{}}function Oe(e,t){let n=`/sdcard/mcp_screenshot_${Date.now()}.png`;h(`adb -s "${e}" shell screencap -p "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),h(`adb -s "${e}" pull "${n}" "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),h(`adb -s "${e}" shell rm "${n}"`,{timeout:5e3,stdio:"ignore"})}function Le(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 Q(e){let t=g();if(!t.length)return{type:"text",text:"No active simulators or emulators found. Start a simulator (iOS) or emulator (Android) first."};let n=w(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=Z.join(Y.tmpdir(),`expo-mcp-screenshot-${Date.now()}.png`);try{n.platform==="ios"?Ce(n.id,r):Oe(n.id,r);let o=v.readFileSync(r),i=o.toString("base64"),s=Le(o);return v.unlinkSync(r),{type:"image",data:i,mimeType:"image/png",width:s?.width??0,height:s?.height??0}}catch(o){try{v.unlinkSync(r)}catch{}return{type:"text",text:`Screenshot failed: ${o instanceof Error?o.message:String(o)}`}}}import{z as m}from"zod";import{execSync as S}from"child_process";import{spawn as Ae}from"child_process";var p=null,M=null;function R(e){p&&!p.killed&&M===e||(p&&!p.killed&&p.kill(),p=Ae("idb_companion",["--udid",e],{detached:!1,stdio:"ignore"}),M=e,p.on("exit",()=>{p=null,M=null}),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,800))}process.on("exit",()=>p?.kill());process.on("SIGTERM",()=>{p?.kill(),process.exit(0)});process.on("SIGINT",()=>{p?.kill(),process.exit(0)});var ee=m.object({x:m.number().int().describe("X coordinate in points/pixels"),y:m.number().int().describe("Y coordinate in points/pixels"),device_id:m.string().optional(),platform:m.enum(["ios","android"]).optional()}),te=m.object({x1:m.number().int(),y1:m.number().int(),x2:m.number().int(),y2:m.number().int(),duration_ms:m.number().int().min(50).max(5e3).optional().default(300),device_id:m.string().optional(),platform:m.enum(["ios","android"]).optional()});function ne(){try{return S("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function Ne(e,t,n){if(ne()){R(e),S(`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{S(`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
|
-
`)}`}var u=new
|
|
18
|
+
Or: brew install facebook/fb/idb-companion`)}function De(e,t,n){S(`adb -s "${e}" shell input tap ${t} ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function Ie(e,t,n,r,o,i){if(ne()){R(e);let s=(i/1e3).toFixed(2);S(`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 Pe(e,t,n,r,o,i){S(`adb -s "${e}" shell input swipe ${t} ${n} ${r} ${o} ${i}`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function re(e){let t=g();if(!t.length)return"No active simulators or emulators found.";let n=w(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"?Ne(n.id,e.x,e.y):De(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 oe(e){let t=g();if(!t.length)return"No active simulators or emulators found.";let n=w(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"?(Ie(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.`):(Pe(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)}`}}function ie(){let e=g();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}):
|
|
20
|
+
${t.join(`
|
|
21
|
+
`)}`}var u=new Fe({name:"expo-metro-mcp",version:"0.1.0"});u.registerTool("get_logs",{description:"Fetch recent logs from the Metro dev server buffer. Supports filtering by level and time.",inputSchema:O.shape},async e=>({content:[{type:"text",text:L(e)}]}));u.registerTool("get_errors",{description:"Fetch recent errors from the Metro dev server buffer, with stack traces.",inputSchema:N.shape},async e=>({content:[{type:"text",text:D(e)}]}));u.registerTool("get_status",{description:"Check the connection status of the Metro dev server and buffer statistics."},async()=>({content:[{type:"text",text:I()}]}));u.registerTool("connect",{description:"Grab the CDP connection to the Metro dev server. Use this if get_status shows disconnected, or to take over the connection from React Native DevTools."},async()=>({content:[{type:"text",text:await c.grabConnection()}]}));u.registerTool("disconnect",{description:"Release the CDP connection so React Native DevTools can connect. Use this before switching to DevTools. Call connect when you want to reattach."},async()=>(c.disconnect(),{content:[{type:"text",text:"Disconnected. DevTools can now connect freely."}]}));u.registerTool("clear_logs",{description:"Clear the internal log buffer. Useful after resolving an issue."},async()=>({content:[{type:"text",text:P()}]}));u.registerTool("watch_logs",{description:"Listen for incoming logs for a short time window and return all collected entries.",inputSchema:F.shape},async e=>({content:[{type:"text",text:await W(e)}]}));u.registerTool("reload",{description:"Reload the React Native app via Metro."},async()=>(H(),{content:[{type:"text",text:await B()}]}));u.registerTool("resolve_stack",{description:"Resolve a stack trace from the buffer against the Metro source map, showing original file:line instead of bundle offsets. Optionally filter by error message substring.",inputSchema:G.shape},async e=>({content:[{type:"text",text:await J(e)}]}));u.registerTool("list_devices",{description:"List active iOS simulators and Android emulators. Use this to find available devices before taking screenshots or sending taps."},async()=>({content:[{type:"text",text:ie()}]}));u.registerTool("screenshot",{description:"Take a screenshot of the active iOS simulator or Android emulator. Returns the image directly. Optionally specify platform ('ios' or 'android') or device_id if multiple devices are running.",inputSchema:V.shape},async e=>{let t=Q(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}]}});u.registerTool("tap",{description:"Tap at x,y coordinates on the active simulator/emulator. Use screenshot first to determine coordinates. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Optionally specify platform or device_id.",inputSchema:ee.shape},async e=>({content:[{type:"text",text:re(e)}]}));u.registerTool("swipe",{description:"Swipe from one coordinate to another. Requires idb on iOS (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Useful for scrolling lists or dismissing sheets.",inputSchema:te.shape},async e=>({content:[{type:"text",text:oe(e)}]}));async function Be(){c.start();let e=new We;await u.connect(e),process.on("SIGINT",()=>{c.stop(),process.exit(0)}),process.on("SIGTERM",()=>{c.stop(),process.exit(0)})}Be().catch(e=>{process.stderr.write(`Fatal error: ${e}
|
|
22
22
|
`),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.2",
|
|
4
4
|
"description": "MCP server for Expo/React Native development — live logs, stack trace resolution, and simulator/emulator automation via CDP and platform CLIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|